From d8446080cce4020037c8cc934840321b0ab296a0 Mon Sep 17 00:00:00 2001 From: Valdior Date: Wed, 14 May 2025 21:45:16 +0200 Subject: [PATCH] init commit --- .eslintrc.js | 18 + .estlintignore | 5 + .gitignore | 21 + .prettierrc.js | 8 + README.md | 2 + __tests/accounts.playground.gql | 23 + __tests/admin/config.ts | 36 + __tests/admin/helpers/graphql-zeus.ts | 4164 +++++ __tests/admin/index.ts | 0 __tests/app/config.ts | 7 + __tests/app/helpers/graphql-zeus.ts | 6824 +++++++++ __tests/app/helpers/zeus/const.ts | 3093 ++++ __tests/app/helpers/zeus/index.ts | 3420 +++++ __tests/app/index.ts | 16 + __tests/app/src/bookings.ts | 72 + __tests/app/src/workspace.ts | 49 + __tests__/booking.gql | 11 + __tests__/signin.gql | 7 + __tests__/signup.gql | 2 + __tests__/workspace.gql | 66 + config/config.ts | 24 + config/domains.yaml | 19 + lib/__cronjobs/serverless-cron.yaml | 32 + lib/__hooks/index.ts | 29 + lib/seed/.gitignore | 19 + lib/seed/README.md | 73 + lib/seed/__loaders.ts | 46 + lib/seed/__tests/helper.ts | 33 + lib/seed/app-admin.ts | 66 + lib/seed/app.ts | 72 + lib/seed/devops/deploy-domains.js | 117 + lib/seed/devops/deploy-v2.js | 98 + lib/seed/devops/deploy.js | 103 + lib/seed/devops/handler-admin.ts | 10 + lib/seed/devops/handler-app.ts | 9 + lib/seed/devops/handler-hooks.ts | 153 + lib/seed/devops/serverless-admin.yaml | 55 + lib/seed/devops/serverless-app.yaml | 60 + lib/seed/devops/serverless-domains.yaml | 12 + lib/seed/devops/serverless-hooks.yaml | 37 + lib/seed/engine/EngineAccessService.ts | 161 + lib/seed/engine/EngineModel.ts | 277 + lib/seed/engine/EngineSchema.ts | 58 + lib/seed/engine/decorators/db.guard.ts | 40 + lib/seed/engine/decorators/pre.hooks.ts | 0 .../genericResolvers/BaseEngineResolver.ts | 114 + lib/seed/engine/utils/__interface.ts | 76 + lib/seed/engine/utils/crud.utils.ts | 721 + lib/seed/engine/utils/service.utils.ts | 118 + .../streams/schemas/stream.components.ts | 41 + .../streams/schemas/stream.schema.input.ts | 11 + .../utils/streams/schemas/stream.schema.ts | 50 + lib/seed/engine/utils/streams/stream.model.ts | 49 + .../engine/utils/streams/stream.resolver.ts | 0 .../engine/utils/streams/stream.service.ts | 84 + lib/seed/examples/__collections.ts | 9 + .../examples/__cronjobs/serverless-cron.yaml | 37 + .../__hooks/functions/example.hooks.ts | 8 + lib/seed/examples/__hooks/index.ts | 35 + lib/seed/examples/app-rest.ts | 11 + lib/seed/examples/config.ts | 34 + lib/seed/examples/domains.yaml | 17 + .../env/__env.accounts.createAdmin.json | 20 + .../examples/env/__env.firebase.import.json | 170 + .../examples/env/__env.general.import.json | 169 + .../env/__env.mailsettings.import.json | 219 + .../env/__env.notifications.import.json | 51 + lib/seed/examples/list.import.json | 47 + lib/seed/graphql/AccessService.ts | 150 + lib/seed/graphql/BaseGraphModel.ts | 828 + lib/seed/graphql/BaseService.ts | 122 + lib/seed/graphql/Middleware.ts | 204 + lib/seed/graphql/MiddlewareV2.ts | 132 + lib/seed/graphql/Request.ts | 54 + lib/seed/graphql/Server.ts | 102 + lib/seed/graphql/Settings.ts | 202 + .../graphql/baseModels/BaseContentModel.ts | 106 + lib/seed/graphql/baseModels/BaseSeoModel.ts | 103 + .../graphql/baseModels/BaseSeoSimpleModel.ts | 107 + .../baseResolvers/BasePublicResolver.ts | 143 + .../graphql/baseResolvers/BaseResolver.ts | 100 + .../genericResolvers/BaseGenericResolver.ts | 97 + .../graphql/genericResolvers/BaseResolver.ts | 100 + lib/seed/helpers/Context.ts | 12 + lib/seed/helpers/Error.ts | 43 + lib/seed/helpers/FetchHelper.ts | 64 + lib/seed/helpers/Request.ts | 29 + lib/seed/helpers/StringHelper.ts | 28 + lib/seed/helpers/TestsHelper.ts | 44 + lib/seed/helpers/Utils.checks.ts | 14 + lib/seed/helpers/Utils.dates.intervals.ts | 88 + lib/seed/helpers/Utils.dates.ts | 310 + lib/seed/helpers/Utils.string.ts | 38 + lib/seed/helpers/Utils.ts | 64 + lib/seed/interfaces/components.dates.ts | 114 + lib/seed/interfaces/components.errors.ts | 99 + lib/seed/interfaces/components.geo.ts | 192 + .../interfaces/components.notifications.ts | 24 + lib/seed/interfaces/components.texts.ts | 46 + lib/seed/interfaces/components.ts | 318 + lib/seed/interfaces/context.ts | 22 + lib/seed/interfaces/permission.ts | 24 + lib/seed/interfaces/response.ts | 28 + lib/seed/services/auth/FirebaseAuthService.ts | 147 + lib/seed/services/auth/FirebaseService.ts | 211 + .../change-stream/change-stream.components.ts | 66 + .../change-stream/change-stream.model.ts | 68 + .../change-stream/change-stream.resolver.ts | 27 + .../change-stream/change-stream.service.ts | 0 .../services/database/DBRequestService.ts | 561 + lib/seed/services/database/DBService.ts | 61 + .../services/database/FirestoreService.ts | 67 + lib/seed/services/database/LoaderService.ts | 59 + lib/seed/services/email/EmailHelper.ts | 118 + lib/seed/services/email/EmailService.ts | 20 + lib/seed/services/geolocation/README.md | 9 + .../__tests/geolocation.playground.gql | 0 .../geolocation/components/countries.ts | 514 + .../functions/GooglePlaceResolver.ts | 59 + .../geolocation/models/BaseGeoSeoModel.ts | 66 + .../geolocation/models/BaseGeoSimpleModel.ts | 83 + .../geolocation/services/GeolocService.ts | 9 + .../services/GooglePlaceService.ts | 173 + lib/seed/services/hooks/hooks.decorator.ts | 126 + .../helpers/notification.helper.ts | 59 + .../notifications/helpers/slack.helper.ts | 60 + .../notifications/helpers/sms.helper.ts | 63 + .../notifications/notifications.model.ts | 41 + .../schemas/notifications.schema.ts | 82 + .../services/NotificationService.ts | 93 + lib/seed/webpack.config.js | 50 + package.json | 121 + serverless.yaml | 44 + services/api-messaging/README.md | 6 + services/api-messaging/__tests/playground.gql | 67 + services/api-messaging/app.ts | 74 + services/api-messaging/components.ts | 35 + .../functions/messages/message.model.ts | 91 + .../functions/messages/message.resolver.ts | 161 + .../functions/rooms/room.model.ts | 115 + .../functions/rooms/room.resolver.ts | 211 + .../functions/rooms/room.service.ts | 17 + .../api-messaging/serverless-messaging.yaml | 40 + services/api-uploads/README.md | 9 + services/api-uploads/__tests/playground.gql | 25 + services/api-uploads/app.ts | 79 + services/api-uploads/file.model.ts | 92 + services/api-uploads/file.resolver.ts | 59 + services/api-uploads/serverless-upload.yaml | 40 + services/api-uploads/services/ImageService.ts | 53 + services/api-uploads/services/S3Service.ts | 57 + .../api-uploads/services/UploadService.ts | 133 + services/module-accounts/.gitignore | 2 + services/module-accounts/README.md | 19 + .../module-accounts/__tests/accounts.test.ts | 74 + .../__tests/admin.playground.graphql | 5 + .../__tests/playground.graphql | 45 + .../module-accounts/account.components.ts | 140 + services/module-accounts/account.model.ts | 258 + .../module-accounts/account.resolver.admin.ts | 125 + services/module-accounts/account.resolver.ts | 171 + .../env/emails.settings.import.json | 229 + .../env/env.settings.import.json | 51 + .../functions/auth-history/pages.model.ts | 51 + .../auth-history/pages.resolver.admin.ts | 72 + .../schemas/auth-history.schema.ts | 45 + .../auth-history/schemas/pages.input.ts | 16 + .../auth/account.auth.mfa.resolver.ts | 246 + .../resolvers/auth/account.auth.resolver.ts | 271 + .../services/AccountService.ts | 72 + .../services/account.service.helper.ts | 136 + services/module-booking/.gitignore | 2 + services/module-booking/README.md | 23 + .../module-booking/__tests/playground.gql | 11 + services/module-booking/components.errors.ts | 6 + .../functions/bookings/booking.service.ts | 156 + .../bookings/bookings.engine.model.ts | 71 + .../resolvers/bookings.engine.resolver.ts | 42 + .../bookings/schemas/bookings.engine.input.ts | 43 + .../schemas/bookings.engine.schema.ts | 76 + .../module-booking/functions/components.ts | 44 + .../functions/crons/booking.cron.service.ts | 31 + services/module-booking/functions/helper.ts | 63 + services/module-cms/.gitignore | 2 + services/module-cms/README.md | 7 + .../__tests/pages.playground.graphql | 31 + .../project-settings.playground.graphql | 1 + services/module-cms/components.ts | 33 + .../functions/categories/category.model.ts | 80 + .../categories/category.resolver.admin.ts | 28 + .../categories/category.resolver.fields.ts | 15 + .../functions/categories/category.resolver.ts | 26 + .../functions/countries/countries.json | 1254 ++ .../functions/countries/country.components.ts | 177 + .../functions/countries/country.model.ts | 41 + .../functions/countries/country.service.ts | 28 + .../resolvers/country.resolver.admin.ts | 73 + .../countries/resolvers/country.resolver.ts | 44 + .../countries/schemas/country.input.ts | 34 + .../countries/schemas/country.schema.ts | 34 + .../emails-settings.components.ts | 57 + .../emails-settings/emails-settings.model.ts | 191 + .../emails-settings.resolver.ts | 33 + .../errors-settings.components.ts | 0 .../error-settings/errors-settings.model.ts | 91 + .../errors-settings.resolver.ts | 33 + .../module-cms/functions/lists/list.model.ts | 75 + .../functions/lists/list.resolver.admin.ts | 26 + .../functions/lists/list.resolver.fields.ts | 15 + .../functions/lists/list.resolver.ts | 26 + .../module-cms/functions/pages/pages.model.ts | 51 + .../functions/pages/pages.resolver.admin.ts | 72 + .../functions/pages/schemas/pages.input.ts | 16 + .../functions/pages/schemas/pages.schema.ts | 38 + .../project-settings.components.ts | 0 .../project-settings.model.ts | 71 + .../project-settings.resolver.admin.ts | 35 + .../module-cms/import/pages.countries.json | 12690 ++++++++++++++++ services/module-cms/index.ts | 19 + services/module-cms/services/CmsService.ts | 66 + services/module-cms/services/PageService.ts | 32 + .../module-cms/services/TreatmentContent.ts | 172 + services/module-comments/.gitignore | 2 + services/module-comments/README.md | 14 + .../module-comments/__tests/playground.gql | 13 + .../functions/comments/comment.model.ts | 47 + .../functions/comments/comment.service.ts | 111 + .../functions/comments/commentStat.model.ts | 60 + .../comments/resolvers/comment.resolver.ts | 53 + .../comments/schemas/comments.schema.input.ts | 34 + .../comments/schemas/comments.schema.ts | 25 + .../module-comments/functions/components.ts | 83 + .../reviews/resolvers/reviews.resolver.ts | 53 + .../functions/reviews/reviews.model.ts | 52 + .../reviews/schemas/reviews.schema.input.ts | 36 + .../reviews/schemas/reviews.schema.ts | 23 + services/module-favorites/.gitignore | 2 + services/module-favorites/README.md | 34 + .../__tests/favorites.playground.graphql | 7 + services/module-favorites/components.ts | 54 + .../favLikes/favLikes.resolver.admin.ts | 14 + .../functions/favLikes/favLikes.resolver.ts | 211 + services/module-payments/.gitignore | 2 + services/module-payments/README.md | 28 + .../__examples/checkout.resolver.ts | 46 + .../__examples/env.import.json | 102 + .../__examples/stripe.env.json | 85 + .../__handlers/stripeHookHandler.ts | 102 + .../__tests/billingInfo.playground.graphql | 30 + .../__tests/pm.playground.graphql | 18 + .../components/components.errors.ts | 8 + .../components/components.orders.ts | 85 + .../components/components.payments.ts | 205 + .../module-payments/components/components.ts | 62 + .../components/stripe.components.ts | 48 + .../billingInfo.resolver.admin.ts | 14 + .../billingInfo.resolver.fields.ts | 14 + .../billingInfos/billingInfo.resolver.ts | 75 + .../functions/orders/__index.ts | 10 + .../functions/orders/carts/__index.ts | 6 + .../orders/carts/resolvers/cart.resolver.ts | 274 + .../carts/schemas/cart.schema.inputs.ts | 30 + .../orders/carts/schemas/line.schema.ts | 134 + .../functions/orders/helpers/OrderHelper.ts | 35 + .../functions/orders/order.model.ts | 60 + .../orders/resolvers/admin/admin.mutations.ts | 116 + .../orders/resolvers/admin/admin.queries.ts | 46 + .../orders/resolvers/app/app.resolver.ts | 86 + .../orders/resolvers/app/checkout.resolver.ts | 54 + .../resolvers/business/business.input.ts | 7 + .../resolvers/business/business.mutations.ts | 58 + .../resolvers/business/business.queries.ts | 67 + .../resolvers/business/business.service.ts | 23 + .../orders/resolvers/field.resolver.ts | 9 + .../orders/schemas/order.schema.input.ts | 49 + .../functions/orders/schemas/order.schema.ts | 140 + .../orders/schemas/provider.schema.ts | 19 + .../functions/orders/services/CartService.ts | 113 + .../functions/orders/services/LineService.ts | 0 .../functions/orders/services/OrderService.ts | 290 + .../paymentMethods/pm.resolver.admin.ts | 14 + .../paymentMethods/pm.resolver.fields.ts | 14 + .../functions/paymentMethods/pm.resolver.ts | 95 + .../payoutAccounts/pa.resolver.admin.ts | 14 + .../payoutAccounts/pa.resolver.fields.ts | 14 + .../functions/payoutAccounts/pa.resolver.ts | 61 + .../functions/promos/component.ts | 9 + .../functions/promos/promo.model.ts | 171 + .../functions/promos/promo.resolver.admin.ts | 51 + .../functions/promos/promo.resolver.ts | 32 + .../functions/vat/vat.components.ts | 761 + .../functions/vat/vat.model.ts | 104 + .../functions/vat/vat.resolver.admin.ts | 71 + .../functions/vat/vat.resolver.ts | 89 + .../helpers/payments.helpers.ts | 93 + services/module-payments/index.ts | 9 + .../module-payments/serverless-payments.yaml | 46 + .../services/stripe/StripeService.ts | 382 + .../services/stripe/paymentIntent.helper.ts | 14 + .../services/stripe/payoutAccounts.helper.ts | 0 src/__components/components.ts | 157 + src/__indexes/__bootstrap.ts | 29 + src/__indexes/__collections.ts | 11 + src/__indexes/__loaders.ts | 68 + src/__indexes/__resolvers.index.admin.ts | 25 + src/__indexes/__resolvers.index.ts | 50 + src/accounts/account.components.ts | 152 + src/accounts/account.helper.ts | 3 + src/accounts/account.model.ts | 77 + src/accounts/account.resolver.ts | 57 + src/accounts/account.service.ts | 0 src/bookings/resolvers/bookings.resolver.ts | 140 + src/bookings/resolvers/getEventDescription.ts | 18 + src/bookings/schemas/bookings.input.ts | 20 + src/bookings/schemas/bookings.schema.ts | 22 + src/orders/components/checkout.schema.ts | 6 + src/orders/components/components.cart.ts | 10 + src/orders/components/line.schema.ts | 44 + src/orders/order.model.ts | 23 + src/orders/order.resolver.admin.ts | 0 src/orders/order.resolver.ts | 55 + src/orders/services/line.service.ts | 56 + src/orders/services/order.service.ts | 186 + src/workspace/workspace.model.ts | 288 + src/workspace/workspace.resolver.admin.ts | 29 + src/workspace/workspace.resolver.fields.ts | 62 + src/workspace/workspace.resolver.ts | 102 + tsconfig.json | 34 + 328 files changed, 55669 insertions(+) create mode 100644 .eslintrc.js create mode 100644 .estlintignore create mode 100644 .gitignore create mode 100644 .prettierrc.js create mode 100644 README.md create mode 100644 __tests/accounts.playground.gql create mode 100644 __tests/admin/config.ts create mode 100644 __tests/admin/helpers/graphql-zeus.ts create mode 100644 __tests/admin/index.ts create mode 100644 __tests/app/config.ts create mode 100644 __tests/app/helpers/graphql-zeus.ts create mode 100644 __tests/app/helpers/zeus/const.ts create mode 100644 __tests/app/helpers/zeus/index.ts create mode 100644 __tests/app/index.ts create mode 100644 __tests/app/src/bookings.ts create mode 100644 __tests/app/src/workspace.ts create mode 100644 __tests__/booking.gql create mode 100644 __tests__/signin.gql create mode 100644 __tests__/signup.gql create mode 100644 __tests__/workspace.gql create mode 100644 config/config.ts create mode 100644 config/domains.yaml create mode 100644 lib/__cronjobs/serverless-cron.yaml create mode 100644 lib/__hooks/index.ts create mode 100644 lib/seed/.gitignore create mode 100644 lib/seed/README.md create mode 100644 lib/seed/__loaders.ts create mode 100644 lib/seed/__tests/helper.ts create mode 100644 lib/seed/app-admin.ts create mode 100644 lib/seed/app.ts create mode 100644 lib/seed/devops/deploy-domains.js create mode 100644 lib/seed/devops/deploy-v2.js create mode 100644 lib/seed/devops/deploy.js create mode 100644 lib/seed/devops/handler-admin.ts create mode 100644 lib/seed/devops/handler-app.ts create mode 100644 lib/seed/devops/handler-hooks.ts create mode 100644 lib/seed/devops/serverless-admin.yaml create mode 100644 lib/seed/devops/serverless-app.yaml create mode 100644 lib/seed/devops/serverless-domains.yaml create mode 100644 lib/seed/devops/serverless-hooks.yaml create mode 100644 lib/seed/engine/EngineAccessService.ts create mode 100644 lib/seed/engine/EngineModel.ts create mode 100644 lib/seed/engine/EngineSchema.ts create mode 100644 lib/seed/engine/decorators/db.guard.ts create mode 100644 lib/seed/engine/decorators/pre.hooks.ts create mode 100644 lib/seed/engine/genericResolvers/BaseEngineResolver.ts create mode 100644 lib/seed/engine/utils/__interface.ts create mode 100644 lib/seed/engine/utils/crud.utils.ts create mode 100644 lib/seed/engine/utils/service.utils.ts create mode 100644 lib/seed/engine/utils/streams/schemas/stream.components.ts create mode 100644 lib/seed/engine/utils/streams/schemas/stream.schema.input.ts create mode 100644 lib/seed/engine/utils/streams/schemas/stream.schema.ts create mode 100644 lib/seed/engine/utils/streams/stream.model.ts create mode 100644 lib/seed/engine/utils/streams/stream.resolver.ts create mode 100644 lib/seed/engine/utils/streams/stream.service.ts create mode 100644 lib/seed/examples/__collections.ts create mode 100644 lib/seed/examples/__cronjobs/serverless-cron.yaml create mode 100644 lib/seed/examples/__hooks/functions/example.hooks.ts create mode 100644 lib/seed/examples/__hooks/index.ts create mode 100644 lib/seed/examples/app-rest.ts create mode 100644 lib/seed/examples/config.ts create mode 100644 lib/seed/examples/domains.yaml create mode 100644 lib/seed/examples/env/__env.accounts.createAdmin.json create mode 100644 lib/seed/examples/env/__env.firebase.import.json create mode 100644 lib/seed/examples/env/__env.general.import.json create mode 100644 lib/seed/examples/env/__env.mailsettings.import.json create mode 100644 lib/seed/examples/env/__env.notifications.import.json create mode 100644 lib/seed/examples/list.import.json create mode 100644 lib/seed/graphql/AccessService.ts create mode 100644 lib/seed/graphql/BaseGraphModel.ts create mode 100644 lib/seed/graphql/BaseService.ts create mode 100644 lib/seed/graphql/Middleware.ts create mode 100644 lib/seed/graphql/MiddlewareV2.ts create mode 100644 lib/seed/graphql/Request.ts create mode 100644 lib/seed/graphql/Server.ts create mode 100644 lib/seed/graphql/Settings.ts create mode 100644 lib/seed/graphql/baseModels/BaseContentModel.ts create mode 100644 lib/seed/graphql/baseModels/BaseSeoModel.ts create mode 100644 lib/seed/graphql/baseModels/BaseSeoSimpleModel.ts create mode 100644 lib/seed/graphql/baseResolvers/BasePublicResolver.ts create mode 100644 lib/seed/graphql/baseResolvers/BaseResolver.ts create mode 100644 lib/seed/graphql/genericResolvers/BaseGenericResolver.ts create mode 100644 lib/seed/graphql/genericResolvers/BaseResolver.ts create mode 100644 lib/seed/helpers/Context.ts create mode 100644 lib/seed/helpers/Error.ts create mode 100644 lib/seed/helpers/FetchHelper.ts create mode 100644 lib/seed/helpers/Request.ts create mode 100644 lib/seed/helpers/StringHelper.ts create mode 100644 lib/seed/helpers/TestsHelper.ts create mode 100644 lib/seed/helpers/Utils.checks.ts create mode 100644 lib/seed/helpers/Utils.dates.intervals.ts create mode 100644 lib/seed/helpers/Utils.dates.ts create mode 100644 lib/seed/helpers/Utils.string.ts create mode 100644 lib/seed/helpers/Utils.ts create mode 100644 lib/seed/interfaces/components.dates.ts create mode 100644 lib/seed/interfaces/components.errors.ts create mode 100644 lib/seed/interfaces/components.geo.ts create mode 100644 lib/seed/interfaces/components.notifications.ts create mode 100644 lib/seed/interfaces/components.texts.ts create mode 100644 lib/seed/interfaces/components.ts create mode 100644 lib/seed/interfaces/context.ts create mode 100644 lib/seed/interfaces/permission.ts create mode 100644 lib/seed/interfaces/response.ts create mode 100644 lib/seed/services/auth/FirebaseAuthService.ts create mode 100644 lib/seed/services/auth/FirebaseService.ts create mode 100644 lib/seed/services/change-stream/change-stream.components.ts create mode 100644 lib/seed/services/change-stream/change-stream.model.ts create mode 100644 lib/seed/services/change-stream/change-stream.resolver.ts create mode 100644 lib/seed/services/change-stream/change-stream.service.ts create mode 100644 lib/seed/services/database/DBRequestService.ts create mode 100644 lib/seed/services/database/DBService.ts create mode 100644 lib/seed/services/database/FirestoreService.ts create mode 100644 lib/seed/services/database/LoaderService.ts create mode 100644 lib/seed/services/email/EmailHelper.ts create mode 100644 lib/seed/services/email/EmailService.ts create mode 100644 lib/seed/services/geolocation/README.md create mode 100644 lib/seed/services/geolocation/__tests/geolocation.playground.gql create mode 100644 lib/seed/services/geolocation/components/countries.ts create mode 100644 lib/seed/services/geolocation/functions/GooglePlaceResolver.ts create mode 100644 lib/seed/services/geolocation/models/BaseGeoSeoModel.ts create mode 100644 lib/seed/services/geolocation/models/BaseGeoSimpleModel.ts create mode 100644 lib/seed/services/geolocation/services/GeolocService.ts create mode 100644 lib/seed/services/geolocation/services/GooglePlaceService.ts create mode 100644 lib/seed/services/hooks/hooks.decorator.ts create mode 100644 lib/seed/services/notifications/helpers/notification.helper.ts create mode 100644 lib/seed/services/notifications/helpers/slack.helper.ts create mode 100644 lib/seed/services/notifications/helpers/sms.helper.ts create mode 100644 lib/seed/services/notifications/notifications.model.ts create mode 100644 lib/seed/services/notifications/schemas/notifications.schema.ts create mode 100644 lib/seed/services/notifications/services/NotificationService.ts create mode 100644 lib/seed/webpack.config.js create mode 100644 package.json create mode 100644 serverless.yaml create mode 100644 services/api-messaging/README.md create mode 100644 services/api-messaging/__tests/playground.gql create mode 100644 services/api-messaging/app.ts create mode 100644 services/api-messaging/components.ts create mode 100644 services/api-messaging/functions/messages/message.model.ts create mode 100644 services/api-messaging/functions/messages/message.resolver.ts create mode 100644 services/api-messaging/functions/rooms/room.model.ts create mode 100644 services/api-messaging/functions/rooms/room.resolver.ts create mode 100644 services/api-messaging/functions/rooms/room.service.ts create mode 100644 services/api-messaging/serverless-messaging.yaml create mode 100644 services/api-uploads/README.md create mode 100644 services/api-uploads/__tests/playground.gql create mode 100644 services/api-uploads/app.ts create mode 100644 services/api-uploads/file.model.ts create mode 100644 services/api-uploads/file.resolver.ts create mode 100644 services/api-uploads/serverless-upload.yaml create mode 100644 services/api-uploads/services/ImageService.ts create mode 100644 services/api-uploads/services/S3Service.ts create mode 100644 services/api-uploads/services/UploadService.ts create mode 100644 services/module-accounts/.gitignore create mode 100644 services/module-accounts/README.md create mode 100644 services/module-accounts/__tests/accounts.test.ts create mode 100644 services/module-accounts/__tests/admin.playground.graphql create mode 100644 services/module-accounts/__tests/playground.graphql create mode 100644 services/module-accounts/account.components.ts create mode 100644 services/module-accounts/account.model.ts create mode 100644 services/module-accounts/account.resolver.admin.ts create mode 100644 services/module-accounts/account.resolver.ts create mode 100644 services/module-accounts/env/emails.settings.import.json create mode 100644 services/module-accounts/env/env.settings.import.json create mode 100644 services/module-accounts/functions/auth-history/pages.model.ts create mode 100644 services/module-accounts/functions/auth-history/pages.resolver.admin.ts create mode 100644 services/module-accounts/functions/auth-history/schemas/auth-history.schema.ts create mode 100644 services/module-accounts/functions/auth-history/schemas/pages.input.ts create mode 100644 services/module-accounts/resolvers/auth/account.auth.mfa.resolver.ts create mode 100644 services/module-accounts/resolvers/auth/account.auth.resolver.ts create mode 100644 services/module-accounts/services/AccountService.ts create mode 100644 services/module-accounts/services/account.service.helper.ts create mode 100644 services/module-booking/.gitignore create mode 100644 services/module-booking/README.md create mode 100644 services/module-booking/__tests/playground.gql create mode 100644 services/module-booking/components.errors.ts create mode 100644 services/module-booking/functions/bookings/booking.service.ts create mode 100644 services/module-booking/functions/bookings/bookings.engine.model.ts create mode 100644 services/module-booking/functions/bookings/resolvers/bookings.engine.resolver.ts create mode 100644 services/module-booking/functions/bookings/schemas/bookings.engine.input.ts create mode 100644 services/module-booking/functions/bookings/schemas/bookings.engine.schema.ts create mode 100644 services/module-booking/functions/components.ts create mode 100644 services/module-booking/functions/crons/booking.cron.service.ts create mode 100644 services/module-booking/functions/helper.ts create mode 100644 services/module-cms/.gitignore create mode 100644 services/module-cms/README.md create mode 100644 services/module-cms/__tests/pages.playground.graphql create mode 100644 services/module-cms/__tests/project-settings.playground.graphql create mode 100644 services/module-cms/components.ts create mode 100644 services/module-cms/functions/categories/category.model.ts create mode 100644 services/module-cms/functions/categories/category.resolver.admin.ts create mode 100644 services/module-cms/functions/categories/category.resolver.fields.ts create mode 100644 services/module-cms/functions/categories/category.resolver.ts create mode 100644 services/module-cms/functions/countries/countries.json create mode 100644 services/module-cms/functions/countries/country.components.ts create mode 100644 services/module-cms/functions/countries/country.model.ts create mode 100644 services/module-cms/functions/countries/country.service.ts create mode 100644 services/module-cms/functions/countries/resolvers/country.resolver.admin.ts create mode 100644 services/module-cms/functions/countries/resolvers/country.resolver.ts create mode 100644 services/module-cms/functions/countries/schemas/country.input.ts create mode 100644 services/module-cms/functions/countries/schemas/country.schema.ts create mode 100644 services/module-cms/functions/emails-settings/emails-settings.components.ts create mode 100644 services/module-cms/functions/emails-settings/emails-settings.model.ts create mode 100644 services/module-cms/functions/emails-settings/emails-settings.resolver.ts create mode 100644 services/module-cms/functions/error-settings/errors-settings.components.ts create mode 100644 services/module-cms/functions/error-settings/errors-settings.model.ts create mode 100644 services/module-cms/functions/error-settings/errors-settings.resolver.ts create mode 100644 services/module-cms/functions/lists/list.model.ts create mode 100644 services/module-cms/functions/lists/list.resolver.admin.ts create mode 100644 services/module-cms/functions/lists/list.resolver.fields.ts create mode 100644 services/module-cms/functions/lists/list.resolver.ts create mode 100644 services/module-cms/functions/pages/pages.model.ts create mode 100644 services/module-cms/functions/pages/pages.resolver.admin.ts create mode 100644 services/module-cms/functions/pages/schemas/pages.input.ts create mode 100644 services/module-cms/functions/pages/schemas/pages.schema.ts create mode 100644 services/module-cms/functions/project-settings/project-settings.components.ts create mode 100644 services/module-cms/functions/project-settings/project-settings.model.ts create mode 100644 services/module-cms/functions/project-settings/project-settings.resolver.admin.ts create mode 100644 services/module-cms/import/pages.countries.json create mode 100644 services/module-cms/index.ts create mode 100644 services/module-cms/services/CmsService.ts create mode 100644 services/module-cms/services/PageService.ts create mode 100644 services/module-cms/services/TreatmentContent.ts create mode 100644 services/module-comments/.gitignore create mode 100644 services/module-comments/README.md create mode 100644 services/module-comments/__tests/playground.gql create mode 100644 services/module-comments/functions/comments/comment.model.ts create mode 100644 services/module-comments/functions/comments/comment.service.ts create mode 100644 services/module-comments/functions/comments/commentStat.model.ts create mode 100644 services/module-comments/functions/comments/resolvers/comment.resolver.ts create mode 100644 services/module-comments/functions/comments/schemas/comments.schema.input.ts create mode 100644 services/module-comments/functions/comments/schemas/comments.schema.ts create mode 100644 services/module-comments/functions/components.ts create mode 100644 services/module-comments/functions/reviews/resolvers/reviews.resolver.ts create mode 100644 services/module-comments/functions/reviews/reviews.model.ts create mode 100644 services/module-comments/functions/reviews/schemas/reviews.schema.input.ts create mode 100644 services/module-comments/functions/reviews/schemas/reviews.schema.ts create mode 100644 services/module-favorites/.gitignore create mode 100644 services/module-favorites/README.md create mode 100644 services/module-favorites/__tests/favorites.playground.graphql create mode 100644 services/module-favorites/components.ts create mode 100644 services/module-favorites/functions/favLikes/favLikes.resolver.admin.ts create mode 100644 services/module-favorites/functions/favLikes/favLikes.resolver.ts create mode 100644 services/module-payments/.gitignore create mode 100644 services/module-payments/README.md create mode 100644 services/module-payments/__examples/checkout.resolver.ts create mode 100644 services/module-payments/__examples/env.import.json create mode 100644 services/module-payments/__examples/stripe.env.json create mode 100644 services/module-payments/__handlers/stripeHookHandler.ts create mode 100644 services/module-payments/__tests/billingInfo.playground.graphql create mode 100644 services/module-payments/__tests/pm.playground.graphql create mode 100644 services/module-payments/components/components.errors.ts create mode 100644 services/module-payments/components/components.orders.ts create mode 100644 services/module-payments/components/components.payments.ts create mode 100644 services/module-payments/components/components.ts create mode 100644 services/module-payments/components/stripe.components.ts create mode 100644 services/module-payments/functions/billingInfos/billingInfo.resolver.admin.ts create mode 100644 services/module-payments/functions/billingInfos/billingInfo.resolver.fields.ts create mode 100644 services/module-payments/functions/billingInfos/billingInfo.resolver.ts create mode 100644 services/module-payments/functions/orders/__index.ts create mode 100644 services/module-payments/functions/orders/carts/__index.ts create mode 100644 services/module-payments/functions/orders/carts/resolvers/cart.resolver.ts create mode 100644 services/module-payments/functions/orders/carts/schemas/cart.schema.inputs.ts create mode 100644 services/module-payments/functions/orders/carts/schemas/line.schema.ts create mode 100644 services/module-payments/functions/orders/helpers/OrderHelper.ts create mode 100644 services/module-payments/functions/orders/order.model.ts create mode 100644 services/module-payments/functions/orders/resolvers/admin/admin.mutations.ts create mode 100644 services/module-payments/functions/orders/resolvers/admin/admin.queries.ts create mode 100644 services/module-payments/functions/orders/resolvers/app/app.resolver.ts create mode 100644 services/module-payments/functions/orders/resolvers/app/checkout.resolver.ts create mode 100644 services/module-payments/functions/orders/resolvers/business/business.input.ts create mode 100644 services/module-payments/functions/orders/resolvers/business/business.mutations.ts create mode 100644 services/module-payments/functions/orders/resolvers/business/business.queries.ts create mode 100644 services/module-payments/functions/orders/resolvers/business/business.service.ts create mode 100644 services/module-payments/functions/orders/resolvers/field.resolver.ts create mode 100644 services/module-payments/functions/orders/schemas/order.schema.input.ts create mode 100644 services/module-payments/functions/orders/schemas/order.schema.ts create mode 100644 services/module-payments/functions/orders/schemas/provider.schema.ts create mode 100644 services/module-payments/functions/orders/services/CartService.ts create mode 100644 services/module-payments/functions/orders/services/LineService.ts create mode 100644 services/module-payments/functions/orders/services/OrderService.ts create mode 100644 services/module-payments/functions/paymentMethods/pm.resolver.admin.ts create mode 100644 services/module-payments/functions/paymentMethods/pm.resolver.fields.ts create mode 100644 services/module-payments/functions/paymentMethods/pm.resolver.ts create mode 100644 services/module-payments/functions/payoutAccounts/pa.resolver.admin.ts create mode 100644 services/module-payments/functions/payoutAccounts/pa.resolver.fields.ts create mode 100644 services/module-payments/functions/payoutAccounts/pa.resolver.ts create mode 100644 services/module-payments/functions/promos/component.ts create mode 100644 services/module-payments/functions/promos/promo.model.ts create mode 100644 services/module-payments/functions/promos/promo.resolver.admin.ts create mode 100644 services/module-payments/functions/promos/promo.resolver.ts create mode 100644 services/module-payments/functions/vat/vat.components.ts create mode 100644 services/module-payments/functions/vat/vat.model.ts create mode 100644 services/module-payments/functions/vat/vat.resolver.admin.ts create mode 100644 services/module-payments/functions/vat/vat.resolver.ts create mode 100644 services/module-payments/helpers/payments.helpers.ts create mode 100644 services/module-payments/index.ts create mode 100644 services/module-payments/serverless-payments.yaml create mode 100644 services/module-payments/services/stripe/StripeService.ts create mode 100644 services/module-payments/services/stripe/paymentIntent.helper.ts create mode 100644 services/module-payments/services/stripe/payoutAccounts.helper.ts create mode 100644 src/__components/components.ts create mode 100644 src/__indexes/__bootstrap.ts create mode 100644 src/__indexes/__collections.ts create mode 100644 src/__indexes/__loaders.ts create mode 100644 src/__indexes/__resolvers.index.admin.ts create mode 100644 src/__indexes/__resolvers.index.ts create mode 100644 src/accounts/account.components.ts create mode 100644 src/accounts/account.helper.ts create mode 100644 src/accounts/account.model.ts create mode 100644 src/accounts/account.resolver.ts create mode 100644 src/accounts/account.service.ts create mode 100644 src/bookings/resolvers/bookings.resolver.ts create mode 100644 src/bookings/resolvers/getEventDescription.ts create mode 100644 src/bookings/schemas/bookings.input.ts create mode 100644 src/bookings/schemas/bookings.schema.ts create mode 100644 src/orders/components/checkout.schema.ts create mode 100644 src/orders/components/components.cart.ts create mode 100644 src/orders/components/line.schema.ts create mode 100644 src/orders/order.model.ts create mode 100644 src/orders/order.resolver.admin.ts create mode 100644 src/orders/order.resolver.ts create mode 100644 src/orders/services/line.service.ts create mode 100644 src/orders/services/order.service.ts create mode 100644 src/workspace/workspace.model.ts create mode 100644 src/workspace/workspace.resolver.admin.ts create mode 100644 src/workspace/workspace.resolver.fields.ts create mode 100644 src/workspace/workspace.resolver.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..50092b9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + parser: '@typescript-eslint/parser', // Specifies the ESLint parser + extends: [ + 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin + 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + ], + parserOptions: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + }, + rules: { + // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + // e.g. "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any":"off", + "@typescript-eslint/interface-name-prefix":"off" + }, +}; \ No newline at end of file diff --git a/.estlintignore b/.estlintignore new file mode 100644 index 0000000..5d64e8f --- /dev/null +++ b/.estlintignore @@ -0,0 +1,5 @@ +.webpack/* + +schema.d.ts +validation.ts +redoc-static.html \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89a5c55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules +.DS_Store +package-lock.json + +.build +.webpack + +*/**/.env +*/**/postman.json +*/**/schema.d.ts +*/**/*.d.ts + +src/config/.env.json + +.serverless/ + +config/.env.json + +handler.ts +.slslogs.txt +.vscode \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..8aa1879 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,8 @@ +module.exports = { + semi: true, + trailingComma: 'all', + singleQuote: true, + printWidth: 150, + tabWidth: 4, + arrowParens: "always" +}; \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce9ad5b --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# workinflex-backend + diff --git a/__tests/accounts.playground.gql b/__tests/accounts.playground.gql new file mode 100644 index 0000000..ae48b65 --- /dev/null +++ b/__tests/accounts.playground.gql @@ -0,0 +1,23 @@ +mutation getCode { + signInEmailCodeRequest(input: { email: "tom@makeit-studio.com" }) { + message + } +} + +mutation signInWithCode { + signInEmailCodeConfirm(input: { email: "tom@makeit-studio.com", code: 731560 }) { + idToken + } +} + +query me { + me { + _id + } +} + +mutation completeRegistration { + completeRegistration(input: { firstName: "Tom", lastName: "Roelants", organisation: "Makeit", position: "Dev" }) { + _id + } +} diff --git a/__tests/admin/config.ts b/__tests/admin/config.ts new file mode 100644 index 0000000..a329877 --- /dev/null +++ b/__tests/admin/config.ts @@ -0,0 +1,36 @@ +import { Thunder } from './helpers/graphql-zeus'; +import fetch from 'node-fetch'; + +const url = 'http://localhost:4001'; +const token = + 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjVmOTcxMmEwODczMTcyMGQ2NmZkNGEyYTU5MmU0ZGZjMmI1ZGU1OTUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vc29mdHdhcmUtdGVhbS1zdGFnaW5nIiwiYXVkIjoic29mdHdhcmUtdGVhbS1zdGFnaW5nIiwiYXV0aF90aW1lIjoxNjEwNzE1ODA4LCJ1c2VyX2lkIjoiTDNpQ3RkZDRzb2djYVJ1Y1RXbVVJNjM0aWo1MyIsInN1YiI6IkwzaUN0ZGQ0c29nY2FSdWNUV21VSTYzNGlqNTMiLCJpYXQiOjE2MTA3MTU4MDgsImV4cCI6MTYxMDcxOTQwOCwiZW1haWwiOiJzc3llZC5tYWtlaXRAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsic3N5ZWQubWFrZWl0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6ImN1c3RvbSJ9fQ.o3HXVp5lrIk_bzaTqb-aA7bpRJLb-Jp7lPz7_O7PQdsOaw5-L066HFtp5I-meS2CdpvUXy1REvAntGEeTNFukBzOkcRjBYagaxwUd_KpZOOoru5wpvTW8jJXlzmQXmDbHm2zfmzSCxsmrdZz62p_mKKJgGw5QTZB_LxUqTo6v0rhboXOowTzvAaHf1iwSU84JfrZwKU4xd0031RZPFbi6NDgHprYIDx7L5Fsl6bQhXDEXELDe4RNYQ5b0CKXa8rdJE9rBuroGJAJhB42_iwDwCa07FwBLIJzXefKqI97s15s23RzVT5o81EtIDOlOI6nBhucsad4boxacT6VBuS_vw'; + +export const thunder = Thunder(async (query) => { + const headers: any = { + 'Content-Type': 'application/json', + }; + + if (token) headers.authorization = token; + + const response = await fetch(url, { + body: JSON.stringify({ query }), + method: 'POST', + headers: headers, + }); + if (!response.ok) { + return new Promise((resolve, reject) => { + response + .text() + .then((text) => { + try { + reject(JSON.parse(text)); + } catch (err) { + reject(text); + } + }) + .catch(reject); + }); + } + const json = await response.json(); + return json.data; +}); diff --git a/__tests/admin/helpers/graphql-zeus.ts b/__tests/admin/helpers/graphql-zeus.ts new file mode 100644 index 0000000..925a88a --- /dev/null +++ b/__tests/admin/helpers/graphql-zeus.ts @@ -0,0 +1,4164 @@ +/* tslint:disable */ +/* eslint-disable */ + +export type ValueTypes = { + ["AccountGenderEnum"]:AccountGenderEnum; + ["AccountModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + types?:true, + organisationIds?:true, + email?:true, + userName?:true, + firstName?:true, + lastName?:true, + profilePicture?:ValueTypes["ImageComponent"], + phoneNumber?:true, + gender?:true, + birthDate?:true, + getAge?:true, + address?:ValueTypes["AddressComponent"], + language?:true, + settings?:ValueTypes["SettingsComponent"], + organisation?:true, + position?:true, + profileCompleted?:true, + __typename?: true +}>; + ["AccountTypeEnum"]:AccountTypeEnum; + ["AddressComponent"]: AliasType<{ + number?:true, + street?:true, + streetBis?:true, + floor?:true, + box?:true, + zip?:true, + state?:true, + city?:true, + country?:true, + __typename?: true +}>; + ["AddressInput"]: { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +}; + ["AvailableTranslation"]:AvailableTranslation; + ["CategoryModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:ValueTypes["TranslatableComponent"], + teaser?:ValueTypes["TranslatableComponent"], + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:ValueTypes["TranslatableComponent"], + seo?:ValueTypes["SEOField"], + urls?:ValueTypes["TranslatableComponent"], + ressourceType?:true, + colorCode?:true, + __typename?: true +}>; + ["CompleteAccountInput"]: { + firstName:string, + lastName:string, + organisation:string, + position:string +}; + /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +["DateTime"]:unknown; + ["EditAccountInput"]: { + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ValueTypes["ImageInput"], + phoneNumber?:string, + gender?:ValueTypes["AccountGenderEnum"], + birthDate?:ValueTypes["DateTime"], + address?:ValueTypes["AddressInput"], + language?:ValueTypes["AvailableTranslation"], + settings?:ValueTypes["SettingsInput"][], + organisation?:string, + position?:string +}; + ["EditCategoryInput"]: { + ressourceType:ValueTypes["RessourceEnum"], + title:ValueTypes["TranslatableInput"], + content?:ValueTypes["TranslatableInput"], + colorCode?:string +}; + ["EditEmailSettingsInput"]: { + subject?:ValueTypes["TranslatableInput"], + body?:ValueTypes["TranslatableInput"], + type?:string, + custom?:boolean, + templateId?:ValueTypes["TranslatableInput"], + fromEmail:string, + fromName:string, + replyToEmail:string, + cci?:string[], + smsSettings?:ValueTypes["SmsSettingsComponentInput"], + slackSettings?:ValueTypes["SlackSettingsComponentInput"] +}; + ["EditListInput"]: { + title?:ValueTypes["TranslatableInput"], + teaser?:ValueTypes["TranslatableInput"], + cover?:ValueTypes["ImageInput"], + thumbnail?:ValueTypes["ImageInput"], + content?:ValueTypes["TranslatableInput"], + seo?:ValueTypes["SEOInput"], + urls?:ValueTypes["TranslatableInput"], + ressourceType?:ValueTypes["ListEnum"], + colorCode?:string +}; + ["EditPageInput"]: { + title?:string, + label?:string, + content?:ValueTypes["JSONObject"], + position?:number, + toComplete?:boolean +}; + ["EditProjectSettingsInput"]: { + value?:string +}; + ["EmailSettingsModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + type?:true, + key?:true, + subject?:ValueTypes["TranslatableComponent"], + body?:ValueTypes["TranslatableComponent"], + custom?:true, + templateId?:ValueTypes["TranslatableComponent"], + fromEmail?:true, + fromName?:true, + replyToEmail?:true, + cci?:true, + availableFields?:true, + templateSample?:true, + smsSettings?:ValueTypes["SmsSettingsComponent"], + pushSettings?:ValueTypes["PushNotifComponent"], + slackSettings?:ValueTypes["SlackSettingsComponent"], + __typename?: true +}>; + ["EnginePathComponent"]: AliasType<{ + ressourceModel?:true, + ressourceId?:true, + __typename?: true +}>; + ["FirebaseTokenResult"]: AliasType<{ + localId?:true, + email?:true, + displayName?:true, + idToken?:true, + registered?:true, + refreshToken?:true, + expiresIn?:true, + __typename?: true +}>; + ["GetArgs"]: { + limit:number, + skip:number, + sort?:string +}; + ["IEngineSchema"]:AliasType<{ + _id?:true, + organisationId?:true, + paths?:ValueTypes["EnginePathComponent"], + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true; + ['...on NaceSchema']?: Omit; + ['...on PageSchema']?: Omit; + __typename?: true +}>; + ["ImageComponent"]: AliasType<{ + title?:true, + fileType?:true, + large?:true, + medium?:true, + small?:true, + __typename?: true +}>; + ["ImageInput"]: { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +}; + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +["JSONObject"]:unknown; + ["LevelEnum"]:LevelEnum; + ["LinkEmailInput"]: { + email:string, + password:string, + idToken:string +}; + ["ListEnum"]:ListEnum; + ["ListModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:ValueTypes["TranslatableComponent"], + teaser?:ValueTypes["TranslatableComponent"], + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:ValueTypes["TranslatableComponent"], + seo?:ValueTypes["SEOField"], + urls?:ValueTypes["TranslatableComponent"], + ressourceType?:true, + colorCode?:true, + __typename?: true +}>; + ["LoginInput"]: { + email:string, + password:string +}; + ["MetaBy"]: AliasType<{ + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + __typename?: true +}>; + ["MetaPermissions"]: AliasType<{ + r?:true, + w?:true, + d?:true, + __typename?: true +}>; + ["ModelLoadersEnum"]:ModelLoadersEnum; + ["Mutation"]: AliasType<{ +updateMe?: [{ input:ValueTypes["EditAccountInput"]},ValueTypes["AccountModel"]], +updateMeEmail?: [{ input:ValueTypes["NewEmailInput"]},ValueTypes["AccountModel"]], +updateMePassword?: [{ input:ValueTypes["NewPasswordInput"]},ValueTypes["AccountModel"]], +resetPassword?: [{ input:ValueTypes["ResetPasswordInput"]},ValueTypes["SimpleResult"]], +registerGuest?: [{ otherInfo:ValueTypes["EditAccountInput"], input:ValueTypes["LinkEmailInput"]},ValueTypes["AccountModel"]], +accountsAddOne?: [{ input:ValueTypes["NewAccountInputAdmin"]},ValueTypes["AccountModel"]], +accountsEditOne?: [{ input:ValueTypes["EditAccountInput"], id:string},ValueTypes["AccountModel"]], +accountsDeleteOne?: [{ userType:ValueTypes["AccountTypeEnum"], id:string},ValueTypes["AccountModel"]], +completeRegistration?: [{ input:ValueTypes["CompleteAccountInput"]},ValueTypes["AccountModel"]], +categoriesAddOne?: [{ input:ValueTypes["NewCategoryInput"]},ValueTypes["CategoryModel"]], +categoriesEditOne?: [{ input:ValueTypes["EditCategoryInput"], id:string},ValueTypes["CategoryModel"]], +categoriesDeleteOne?: [{ id:string},ValueTypes["CategoryModel"]], +emailsSettingsAddOne?: [{ input:ValueTypes["NewEmailSettingsInput"]},ValueTypes["EmailSettingsModel"]], +emailsSettingsEditOne?: [{ input:ValueTypes["EditEmailSettingsInput"], id:string},ValueTypes["EmailSettingsModel"]], +emailsSettingsDeleteOne?: [{ id:string},ValueTypes["EmailSettingsModel"]], +projectSettingsAddOne?: [{ input:ValueTypes["NewProjectSettingsInput"]},ValueTypes["ProjectSettingModel"]], +projectSettingsEditOne?: [{ input:ValueTypes["EditProjectSettingsInput"], id:string},ValueTypes["ProjectSettingModel"]], +projectSettingsDeleteOne?: [{ id:string},ValueTypes["ProjectSettingModel"]], +listsAddOne?: [{ input:ValueTypes["NewListInput"]},ValueTypes["ListModel"]], +listsEditOne?: [{ input:ValueTypes["EditListInput"], id:string},ValueTypes["ListModel"]], +listsDeleteOne?: [{ id:string},ValueTypes["ListModel"]], +pagesAddOne?: [{ input:ValueTypes["NewPageInput"]},ValueTypes["PageSchema"]], +pagesEditOne?: [{ input:ValueTypes["EditPageInput"], id:string},ValueTypes["PageSchema"]], +pagesDeleteOne?: [{ id:string},ValueTypes["PageSchema"]], + refreshCaching?:ValueTypes["SuccessResponse"], +naceAddOne?: [{ input:ValueTypes["NaceNewInputSchema"]},ValueTypes["NaceSchema"]], +naceEditOne?: [{ input:ValueTypes["NaceEditInputSchema"], id:string},ValueTypes["NaceSchema"]], +naceDeleteOne?: [{ id:string},ValueTypes["NaceSchema"]], + __typename?: true +}>; + ["NaceEditInputSchema"]: { + name:ValueTypes["TranslatableInput"], + code:string, + macroSelector:string, + description:ValueTypes["TranslatableInput"], + level:ValueTypes["LevelEnum"] +}; + ["NaceNewInputSchema"]: { + name:ValueTypes["TranslatableInput"], + code:string, + macroSelector:string, + description:ValueTypes["TranslatableInput"], + level:ValueTypes["LevelEnum"] +}; + ["NaceSchema"]: AliasType<{ + name?:ValueTypes["TranslatableComponent"], + code?:true, + macroSelector?:true, + description?:ValueTypes["TranslatableComponent"], + level?:true, + _id?:true, + organisationId?:true, + paths?:ValueTypes["EnginePathComponent"], + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + __typename?: true +}>; + ["NewAccountInputAdmin"]: { + type:ValueTypes["AccountTypeEnum"], + organisationIds?:string[], + email:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ValueTypes["ImageInput"], + phoneNumber?:string, + gender?:ValueTypes["AccountGenderEnum"], + birthDate?:ValueTypes["DateTime"], + address?:ValueTypes["AddressInput"], + language?:ValueTypes["AvailableTranslation"], + settings?:ValueTypes["SettingsInput"][] +}; + ["NewCategoryInput"]: { + ressourceType:ValueTypes["RessourceEnum"], + title:ValueTypes["TranslatableInput"], + content?:ValueTypes["TranslatableInput"], + colorCode?:string +}; + ["NewEmailInput"]: { + newEmail:string +}; + ["NewEmailSettingsInput"]: { + subject?:ValueTypes["TranslatableInput"], + body?:ValueTypes["TranslatableInput"], + type?:string, + key:string, + custom?:boolean, + templateId?:ValueTypes["TranslatableInput"], + fromEmail:string, + fromName:string, + replyToEmail:string, + cci?:string[], + smsSettings?:ValueTypes["SmsSettingsComponentInput"], + slackSettings?:ValueTypes["SlackSettingsComponentInput"] +}; + ["NewListInput"]: { + title:ValueTypes["TranslatableInput"], + teaser?:ValueTypes["TranslatableInput"], + cover?:ValueTypes["ImageInput"], + thumbnail?:ValueTypes["ImageInput"], + content?:ValueTypes["TranslatableInput"], + seo?:ValueTypes["SEOInput"], + urls?:ValueTypes["TranslatableInput"], + ressourceType:ValueTypes["ListEnum"], + colorCode?:string +}; + ["NewPageInput"]: { + title:string, + label?:string, + content?:ValueTypes["JSONObject"], + position?:number, + toComplete?:boolean +}; + ["NewPasswordInput"]: { + oldPassword:string, + newPassword:string, + newPasswordConfirmation:string +}; + ["NewProjectSettingsInput"]: { + key:string, + value:string +}; + ["PageSchema"]: AliasType<{ + title?:true, + label?:true, + content?:true, + position?:true, + toComplete?:true, + _id?:true, + organisationId?:true, + paths?:ValueTypes["EnginePathComponent"], + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + __typename?: true +}>; + ["ProjectSettingModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + type?:true, + key?:true, + value?:true, + __typename?: true +}>; + ["PushNotifComponent"]: AliasType<{ + subject?:ValueTypes["TranslatableComponent"], + text?:ValueTypes["TranslatableComponent"], + __typename?: true +}>; + ["Query"]: AliasType<{ + me?:ValueTypes["AccountModel"], +login?: [{ refreshToken?:string, creds?:ValueTypes["LoginInput"]},ValueTypes["FirebaseTokenResult"]], +magicLink?: [{ input:ValueTypes["ResetPasswordInput"]},ValueTypes["SimpleResult"]], +accountsGetOne?: [{ id:string},ValueTypes["AccountModel"]], +accountsGetMany?: [{ pagination?:ValueTypes["GetArgs"]},ValueTypes["AccountModel"]], +categoriesGetOne?: [{ id:string},ValueTypes["CategoryModel"]], +categoriesGetMany?: [{ search?:string, ressourceType:ValueTypes["RessourceEnum"], pagination?:ValueTypes["GetArgs"]},ValueTypes["CategoryModel"]], +categoriesGetCount?: [{ search?:string, ressourceType:ValueTypes["RessourceEnum"], pagination?:ValueTypes["GetArgs"]},true], +emailsSettingsGetOne?: [{ id:string},ValueTypes["EmailSettingsModel"]], +emailsSettingsGetMany?: [{ search?:string, pagination?:ValueTypes["GetArgs"]},ValueTypes["EmailSettingsModel"]], +emailsSettingsGetCount?: [{ search?:string, pagination?:ValueTypes["GetArgs"]},true], +projectSettingsGetOne?: [{ id:string},ValueTypes["ProjectSettingModel"]], +projectSettingsGetMany?: [{ _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], type?:ValueTypes["SettingsType"]},ValueTypes["ProjectSettingModel"]], +projectSettingsGetCount?: [{ _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], type?:ValueTypes["SettingsType"]},true], +listsGetOne?: [{ id:string},ValueTypes["ListModel"]], +listsGetMany?: [{ search?:string, ressourceType:ValueTypes["ListEnum"], pagination?:ValueTypes["GetArgs"]},ValueTypes["ListModel"]], +listsGetCount?: [{ search?:string, ressourceType:ValueTypes["ListEnum"], pagination?:ValueTypes["GetArgs"]},true], +pagesGetOne?: [{ id:string},ValueTypes["PageSchema"]], +pagesGetMany?: [{ _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"]},ValueTypes["PageSchema"]], +pagesGetCount?: [{ _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"]},true], +naceGetOne?: [{ id:string},ValueTypes["NaceSchema"]], +naceGetMany?: [{ _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], name?:ValueTypes["TranslatableInput"], code?:string, macroSelector?:string, level?:ValueTypes["LevelEnum"]},ValueTypes["NaceSchema"]], +naceGetCount?: [{ _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], name?:ValueTypes["TranslatableInput"], code?:string, macroSelector?:string, level?:ValueTypes["LevelEnum"]},true], + __typename?: true +}>; + ["ResetPasswordInput"]: { + email:string +}; + ["RessourceEnum"]:RessourceEnum; + ["SEOField"]: AliasType<{ + title?:ValueTypes["TranslatableComponent"], + description?:ValueTypes["TranslatableComponent"], + keywords?:ValueTypes["TranslatableComponent"], + thumbnail?:ValueTypes["ImageComponent"], + __typename?: true +}>; + ["SEOInput"]: { + title:ValueTypes["TranslatableInput"], + description:ValueTypes["TranslatableInput"], + keywords?:ValueTypes["TranslatableInput"], + thumbnail?:ValueTypes["ImageInput"] +}; + ["SettingsComponent"]: AliasType<{ + type?:true, + email?:true, + pushNotifications?:true, + sms?:true, + __typename?: true +}>; + ["SettingsInput"]: { + type:ValueTypes["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +}; + ["SettingsType"]:SettingsType; + ["SettingsTypeEnum"]:SettingsTypeEnum; + ["SimpleResult"]: AliasType<{ + message?:true, + __typename?: true +}>; + ["SlackActionInfo"]: AliasType<{ + text?:true, + url?:true, + style?:true, + __typename?: true +}>; + ["SlackActionInfoInput"]: { + text:string, + url:string, + style?:ValueTypes["SlackActionStyleEnum"] +}; + ["SlackActionStyleEnum"]:SlackActionStyleEnum; + ["SlackSettingsComponent"]: AliasType<{ + webhookUrls?:true, + username?:true, + text?:true, + icon?:true, + actions?:ValueTypes["SlackActionInfo"], + __typename?: true +}>; + ["SlackSettingsComponentInput"]: { + webhookUrls:string[], + username?:string, + text:string, + icon?:string, + actions?:ValueTypes["SlackActionInfoInput"][] +}; + ["SmsSettingsComponent"]: AliasType<{ + text?:ValueTypes["TranslatableComponent"], + __typename?: true +}>; + ["SmsSettingsComponentInput"]: { + text:ValueTypes["TranslatableInput"] +}; + ["SuccessResponse"]: AliasType<{ + success?:true, + __typename?: true +}>; + ["TranslatableComponent"]: AliasType<{ + en?:true, + __typename?: true +}>; + ["TranslatableInput"]: { + en?:string +} + } + +export type PartialObjects = { + ["AccountGenderEnum"]:AccountGenderEnum, + ["AccountModel"]: { + __typename?: "AccountModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + types?:PartialObjects["AccountTypeEnum"][], + organisationIds?:string[], + email?:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:PartialObjects["ImageComponent"], + phoneNumber?:string, + gender?:PartialObjects["AccountGenderEnum"], + birthDate?:PartialObjects["DateTime"], + getAge?:number, + address?:PartialObjects["AddressComponent"], + language?:PartialObjects["AvailableTranslation"], + settings?:PartialObjects["SettingsComponent"][], + organisation?:string, + position?:string, + profileCompleted?:boolean + }, + ["AccountTypeEnum"]:AccountTypeEnum, + ["AddressComponent"]: { + __typename?: "AddressComponent"; + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city?:string, + country?:string + }, + ["AddressInput"]: { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +}, + ["AvailableTranslation"]:AvailableTranslation, + ["CategoryModel"]: { + __typename?: "CategoryModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + title?:PartialObjects["TranslatableComponent"], + teaser?:PartialObjects["TranslatableComponent"], + cover?:PartialObjects["ImageComponent"], + thumbnail?:PartialObjects["ImageComponent"], + extraImages?:PartialObjects["ImageComponent"][], + content?:PartialObjects["TranslatableComponent"], + seo?:PartialObjects["SEOField"], + urls?:PartialObjects["TranslatableComponent"], + ressourceType?:PartialObjects["RessourceEnum"], + colorCode?:string + }, + ["CompleteAccountInput"]: { + firstName:string, + lastName:string, + organisation:string, + position:string +}, + /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +["DateTime"]:any, + ["EditAccountInput"]: { + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:PartialObjects["ImageInput"], + phoneNumber?:string, + gender?:PartialObjects["AccountGenderEnum"], + birthDate?:PartialObjects["DateTime"], + address?:PartialObjects["AddressInput"], + language?:PartialObjects["AvailableTranslation"], + settings?:PartialObjects["SettingsInput"][], + organisation?:string, + position?:string +}, + ["EditCategoryInput"]: { + ressourceType:PartialObjects["RessourceEnum"], + title:PartialObjects["TranslatableInput"], + content?:PartialObjects["TranslatableInput"], + colorCode?:string +}, + ["EditEmailSettingsInput"]: { + subject?:PartialObjects["TranslatableInput"], + body?:PartialObjects["TranslatableInput"], + type?:string, + custom?:boolean, + templateId?:PartialObjects["TranslatableInput"], + fromEmail:string, + fromName:string, + replyToEmail:string, + cci?:string[], + smsSettings?:PartialObjects["SmsSettingsComponentInput"], + slackSettings?:PartialObjects["SlackSettingsComponentInput"] +}, + ["EditListInput"]: { + title?:PartialObjects["TranslatableInput"], + teaser?:PartialObjects["TranslatableInput"], + cover?:PartialObjects["ImageInput"], + thumbnail?:PartialObjects["ImageInput"], + content?:PartialObjects["TranslatableInput"], + seo?:PartialObjects["SEOInput"], + urls?:PartialObjects["TranslatableInput"], + ressourceType?:PartialObjects["ListEnum"], + colorCode?:string +}, + ["EditPageInput"]: { + title?:string, + label?:string, + content?:PartialObjects["JSONObject"], + position?:number, + toComplete?:boolean +}, + ["EditProjectSettingsInput"]: { + value?:string +}, + ["EmailSettingsModel"]: { + __typename?: "EmailSettingsModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + type?:PartialObjects["SettingsType"], + key?:string, + subject?:PartialObjects["TranslatableComponent"], + body?:PartialObjects["TranslatableComponent"], + custom?:boolean, + templateId?:PartialObjects["TranslatableComponent"], + fromEmail?:string, + fromName?:string, + replyToEmail?:string, + cci?:string[], + availableFields?:string[], + templateSample?:string, + smsSettings?:PartialObjects["SmsSettingsComponent"], + pushSettings?:PartialObjects["PushNotifComponent"], + slackSettings?:PartialObjects["SlackSettingsComponent"] + }, + ["EnginePathComponent"]: { + __typename?: "EnginePathComponent"; + ressourceModel?:PartialObjects["ModelLoadersEnum"], + ressourceId?:string + }, + ["FirebaseTokenResult"]: { + __typename?: "FirebaseTokenResult"; + localId?:string, + email?:string, + displayName?:string, + idToken?:string, + registered?:boolean, + refreshToken?:string, + expiresIn?:string + }, + ["GetArgs"]: { + limit:number, + skip:number, + sort?:string +}, + ["IEngineSchema"]:{ + _id?:string; + organisationId?:string; + paths?:PartialObjects["EnginePathComponent"][]; + by?:PartialObjects["MetaBy"]; + permissions?:PartialObjects["MetaPermissions"]; + createdAt?:PartialObjects["DateTime"]; + updatedAt?:PartialObjects["DateTime"] +} & (PartialObjects["NaceSchema"] | PartialObjects["PageSchema"]), + ["ImageComponent"]: { + __typename?: "ImageComponent"; + title?:string, + fileType?:string, + large?:string, + medium?:string, + small?:string + }, + ["ImageInput"]: { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +}, + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +["JSONObject"]:any, + ["LevelEnum"]:LevelEnum, + ["LinkEmailInput"]: { + email:string, + password:string, + idToken:string +}, + ["ListEnum"]:ListEnum, + ["ListModel"]: { + __typename?: "ListModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + title?:PartialObjects["TranslatableComponent"], + teaser?:PartialObjects["TranslatableComponent"], + cover?:PartialObjects["ImageComponent"], + thumbnail?:PartialObjects["ImageComponent"], + extraImages?:PartialObjects["ImageComponent"][], + content?:PartialObjects["TranslatableComponent"], + seo?:PartialObjects["SEOField"], + urls?:PartialObjects["TranslatableComponent"], + ressourceType?:PartialObjects["ListEnum"], + colorCode?:string + }, + ["LoginInput"]: { + email:string, + password:string +}, + ["MetaBy"]: { + __typename?: "MetaBy"; + createdBy?:string, + updatedBy?:string, + deletedBy?:string + }, + ["MetaPermissions"]: { + __typename?: "MetaPermissions"; + r?:string[], + w?:string[], + d?:string[] + }, + ["ModelLoadersEnum"]:ModelLoadersEnum, + ["Mutation"]: { + __typename?: "Mutation"; + updateMe?:PartialObjects["AccountModel"], + updateMeEmail?:PartialObjects["AccountModel"], + updateMePassword?:PartialObjects["AccountModel"], + resetPassword?:PartialObjects["SimpleResult"], + registerGuest?:PartialObjects["AccountModel"], + accountsAddOne?:PartialObjects["AccountModel"], + accountsEditOne?:PartialObjects["AccountModel"], + accountsDeleteOne?:PartialObjects["AccountModel"], + completeRegistration?:PartialObjects["AccountModel"], + categoriesAddOne?:PartialObjects["CategoryModel"], + categoriesEditOne?:PartialObjects["CategoryModel"], + categoriesDeleteOne?:PartialObjects["CategoryModel"], + emailsSettingsAddOne?:PartialObjects["EmailSettingsModel"], + emailsSettingsEditOne?:PartialObjects["EmailSettingsModel"], + emailsSettingsDeleteOne?:PartialObjects["EmailSettingsModel"], + projectSettingsAddOne?:PartialObjects["ProjectSettingModel"], + projectSettingsEditOne?:PartialObjects["ProjectSettingModel"], + projectSettingsDeleteOne?:PartialObjects["ProjectSettingModel"], + listsAddOne?:PartialObjects["ListModel"], + listsEditOne?:PartialObjects["ListModel"], + listsDeleteOne?:PartialObjects["ListModel"], + pagesAddOne?:PartialObjects["PageSchema"], + pagesEditOne?:PartialObjects["PageSchema"], + pagesDeleteOne?:PartialObjects["PageSchema"], + refreshCaching?:PartialObjects["SuccessResponse"], + naceAddOne?:PartialObjects["NaceSchema"], + naceEditOne?:PartialObjects["NaceSchema"], + naceDeleteOne?:PartialObjects["NaceSchema"] + }, + ["NaceEditInputSchema"]: { + name:PartialObjects["TranslatableInput"], + code:string, + macroSelector:string, + description:PartialObjects["TranslatableInput"], + level:PartialObjects["LevelEnum"] +}, + ["NaceNewInputSchema"]: { + name:PartialObjects["TranslatableInput"], + code:string, + macroSelector:string, + description:PartialObjects["TranslatableInput"], + level:PartialObjects["LevelEnum"] +}, + ["NaceSchema"]: { + __typename?: "NaceSchema"; + name?:PartialObjects["TranslatableComponent"], + code?:string, + macroSelector?:string, + description?:PartialObjects["TranslatableComponent"], + level?:PartialObjects["LevelEnum"], + _id?:string, + organisationId?:string, + paths?:PartialObjects["EnginePathComponent"][], + by?:PartialObjects["MetaBy"], + permissions?:PartialObjects["MetaPermissions"], + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"] + }, + ["NewAccountInputAdmin"]: { + type:PartialObjects["AccountTypeEnum"], + organisationIds?:string[], + email:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:PartialObjects["ImageInput"], + phoneNumber?:string, + gender?:PartialObjects["AccountGenderEnum"], + birthDate?:PartialObjects["DateTime"], + address?:PartialObjects["AddressInput"], + language?:PartialObjects["AvailableTranslation"], + settings?:PartialObjects["SettingsInput"][] +}, + ["NewCategoryInput"]: { + ressourceType:PartialObjects["RessourceEnum"], + title:PartialObjects["TranslatableInput"], + content?:PartialObjects["TranslatableInput"], + colorCode?:string +}, + ["NewEmailInput"]: { + newEmail:string +}, + ["NewEmailSettingsInput"]: { + subject?:PartialObjects["TranslatableInput"], + body?:PartialObjects["TranslatableInput"], + type?:string, + key:string, + custom?:boolean, + templateId?:PartialObjects["TranslatableInput"], + fromEmail:string, + fromName:string, + replyToEmail:string, + cci?:string[], + smsSettings?:PartialObjects["SmsSettingsComponentInput"], + slackSettings?:PartialObjects["SlackSettingsComponentInput"] +}, + ["NewListInput"]: { + title:PartialObjects["TranslatableInput"], + teaser?:PartialObjects["TranslatableInput"], + cover?:PartialObjects["ImageInput"], + thumbnail?:PartialObjects["ImageInput"], + content?:PartialObjects["TranslatableInput"], + seo?:PartialObjects["SEOInput"], + urls?:PartialObjects["TranslatableInput"], + ressourceType:PartialObjects["ListEnum"], + colorCode?:string +}, + ["NewPageInput"]: { + title:string, + label?:string, + content?:PartialObjects["JSONObject"], + position?:number, + toComplete?:boolean +}, + ["NewPasswordInput"]: { + oldPassword:string, + newPassword:string, + newPasswordConfirmation:string +}, + ["NewProjectSettingsInput"]: { + key:string, + value:string +}, + ["PageSchema"]: { + __typename?: "PageSchema"; + title?:string, + label?:string, + content?:PartialObjects["JSONObject"], + position?:number, + toComplete?:boolean, + _id?:string, + organisationId?:string, + paths?:PartialObjects["EnginePathComponent"][], + by?:PartialObjects["MetaBy"], + permissions?:PartialObjects["MetaPermissions"], + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"] + }, + ["ProjectSettingModel"]: { + __typename?: "ProjectSettingModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + type?:PartialObjects["SettingsType"], + key?:string, + value?:string + }, + ["PushNotifComponent"]: { + __typename?: "PushNotifComponent"; + subject?:PartialObjects["TranslatableComponent"], + text?:PartialObjects["TranslatableComponent"] + }, + ["Query"]: { + __typename?: "Query"; + me?:PartialObjects["AccountModel"], + login?:PartialObjects["FirebaseTokenResult"], + magicLink?:PartialObjects["SimpleResult"], + accountsGetOne?:PartialObjects["AccountModel"], + accountsGetMany?:PartialObjects["AccountModel"][], + categoriesGetOne?:PartialObjects["CategoryModel"], + categoriesGetMany?:PartialObjects["CategoryModel"][], + categoriesGetCount?:number, + emailsSettingsGetOne?:PartialObjects["EmailSettingsModel"], + emailsSettingsGetMany?:PartialObjects["EmailSettingsModel"][], + emailsSettingsGetCount?:number, + projectSettingsGetOne?:PartialObjects["ProjectSettingModel"], + projectSettingsGetMany?:PartialObjects["ProjectSettingModel"][], + projectSettingsGetCount?:number, + listsGetOne?:PartialObjects["ListModel"], + listsGetMany?:PartialObjects["ListModel"][], + listsGetCount?:number, + pagesGetOne?:PartialObjects["PageSchema"], + pagesGetMany?:PartialObjects["PageSchema"][], + pagesGetCount?:number, + naceGetOne?:PartialObjects["NaceSchema"], + naceGetMany?:PartialObjects["NaceSchema"][], + naceGetCount?:number + }, + ["ResetPasswordInput"]: { + email:string +}, + ["RessourceEnum"]:RessourceEnum, + ["SEOField"]: { + __typename?: "SEOField"; + title?:PartialObjects["TranslatableComponent"], + description?:PartialObjects["TranslatableComponent"], + keywords?:PartialObjects["TranslatableComponent"], + thumbnail?:PartialObjects["ImageComponent"] + }, + ["SEOInput"]: { + title:PartialObjects["TranslatableInput"], + description:PartialObjects["TranslatableInput"], + keywords?:PartialObjects["TranslatableInput"], + thumbnail?:PartialObjects["ImageInput"] +}, + ["SettingsComponent"]: { + __typename?: "SettingsComponent"; + type?:PartialObjects["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean + }, + ["SettingsInput"]: { + type:PartialObjects["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +}, + ["SettingsType"]:SettingsType, + ["SettingsTypeEnum"]:SettingsTypeEnum, + ["SimpleResult"]: { + __typename?: "SimpleResult"; + message?:string + }, + ["SlackActionInfo"]: { + __typename?: "SlackActionInfo"; + text?:string, + url?:string, + style?:PartialObjects["SlackActionStyleEnum"] + }, + ["SlackActionInfoInput"]: { + text:string, + url:string, + style?:PartialObjects["SlackActionStyleEnum"] +}, + ["SlackActionStyleEnum"]:SlackActionStyleEnum, + ["SlackSettingsComponent"]: { + __typename?: "SlackSettingsComponent"; + webhookUrls?:string[], + username?:string, + text?:string, + icon?:string, + actions?:PartialObjects["SlackActionInfo"][] + }, + ["SlackSettingsComponentInput"]: { + webhookUrls:string[], + username?:string, + text:string, + icon?:string, + actions?:PartialObjects["SlackActionInfoInput"][] +}, + ["SmsSettingsComponent"]: { + __typename?: "SmsSettingsComponent"; + text?:PartialObjects["TranslatableComponent"] + }, + ["SmsSettingsComponentInput"]: { + text:PartialObjects["TranslatableInput"] +}, + ["SuccessResponse"]: { + __typename?: "SuccessResponse"; + success?:boolean + }, + ["TranslatableComponent"]: { + __typename?: "TranslatableComponent"; + en?:string + }, + ["TranslatableInput"]: { + en?:string +} + } + +export enum AccountGenderEnum { + m = "m", + f = "f", + other = "other" +} + +export type AccountModel = { + __typename?: "AccountModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + types:AccountTypeEnum[], + organisationIds?:string[], + email:string, + userName?:string, + firstName:string, + lastName:string, + profilePicture?:ImageComponent, + phoneNumber?:string, + gender?:AccountGenderEnum, + birthDate?:DateTime, + getAge?:number, + address?:AddressComponent, + language?:AvailableTranslation, + settings?:SettingsComponent[], + organisation:string, + position:string, + profileCompleted:boolean +} + +export enum AccountTypeEnum { + admin = "admin", + user = "user", + public = "public", + blogAuthor = "blogAuthor", + investor = "investor", + company = "company", + bank = "bank", + consultant = "consultant" +} + +export type AddressComponent = { + __typename?: "AddressComponent", + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +} + +export type AddressInput = { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +} + +export enum AvailableTranslation { + en = "en" +} + +export type CategoryModel = { + __typename?: "CategoryModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + title:TranslatableComponent, + teaser?:TranslatableComponent, + cover?:ImageComponent, + thumbnail?:ImageComponent, + extraImages?:ImageComponent[], + content?:TranslatableComponent, + seo?:SEOField, + urls?:TranslatableComponent, + ressourceType:RessourceEnum, + colorCode?:string +} + +export type CompleteAccountInput = { + firstName:string, + lastName:string, + organisation:string, + position:string +} + +/** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +export type DateTime = any + +export type EditAccountInput = { + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ImageInput, + phoneNumber?:string, + gender?:AccountGenderEnum, + birthDate?:DateTime, + address?:AddressInput, + language?:AvailableTranslation, + settings?:SettingsInput[], + organisation?:string, + position?:string +} + +export type EditCategoryInput = { + ressourceType:RessourceEnum, + title:TranslatableInput, + content?:TranslatableInput, + colorCode?:string +} + +export type EditEmailSettingsInput = { + subject?:TranslatableInput, + body?:TranslatableInput, + type?:string, + custom?:boolean, + templateId?:TranslatableInput, + fromEmail:string, + fromName:string, + replyToEmail:string, + cci?:string[], + smsSettings?:SmsSettingsComponentInput, + slackSettings?:SlackSettingsComponentInput +} + +export type EditListInput = { + title?:TranslatableInput, + teaser?:TranslatableInput, + cover?:ImageInput, + thumbnail?:ImageInput, + content?:TranslatableInput, + seo?:SEOInput, + urls?:TranslatableInput, + ressourceType?:ListEnum, + colorCode?:string +} + +export type EditPageInput = { + title?:string, + label?:string, + content?:JSONObject, + position?:number, + toComplete?:boolean +} + +export type EditProjectSettingsInput = { + value?:string +} + +export type EmailSettingsModel = { + __typename?: "EmailSettingsModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + type:SettingsType, + key:string, + subject?:TranslatableComponent, + body?:TranslatableComponent, + custom:boolean, + templateId?:TranslatableComponent, + fromEmail:string, + fromName:string, + replyToEmail:string, + cci?:string[], + availableFields?:string[], + templateSample?:string, + smsSettings?:SmsSettingsComponent, + pushSettings?:PushNotifComponent, + slackSettings?:SlackSettingsComponent +} + +export type EnginePathComponent = { + __typename?: "EnginePathComponent", + ressourceModel:ModelLoadersEnum, + ressourceId:string +} + +export type FirebaseTokenResult = { + __typename?: "FirebaseTokenResult", + localId:string, + email?:string, + displayName?:string, + idToken:string, + registered?:boolean, + refreshToken:string, + expiresIn:string +} + +export type GetArgs = { + limit:number, + skip:number, + sort?:string +} + +export type IEngineSchema = { + __interface:{ + _id:string, + organisationId?:string, + paths?:EnginePathComponent[], + by?:MetaBy, + permissions?:MetaPermissions, + createdAt:DateTime, + updatedAt:DateTime + }; + __resolve:{ + ['...on NaceSchema']: NaceSchema; + ['...on PageSchema']: PageSchema; + } +} + +export type ImageComponent = { + __typename?: "ImageComponent", + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +} + +export type ImageInput = { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +} + +/** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +export type JSONObject = any + +export enum LevelEnum { + level1 = "level1", + level2 = "level2", + level3 = "level3", + level4 = "level4" +} + +export type LinkEmailInput = { + email:string, + password:string, + idToken:string +} + +export enum ListEnum { + articles = "articles" +} + +export type ListModel = { + __typename?: "ListModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + title:TranslatableComponent, + teaser?:TranslatableComponent, + cover?:ImageComponent, + thumbnail?:ImageComponent, + extraImages?:ImageComponent[], + content?:TranslatableComponent, + seo?:SEOField, + urls?:TranslatableComponent, + ressourceType:ListEnum, + colorCode?:string +} + +export type LoginInput = { + email:string, + password:string +} + +export type MetaBy = { + __typename?: "MetaBy", + createdBy?:string, + updatedBy?:string, + deletedBy?:string +} + +export type MetaPermissions = { + __typename?: "MetaPermissions", + r:string[], + w:string[], + d:string[] +} + +export enum ModelLoadersEnum { + streams = "streams", + notifications = "notifications", + pages = "pages", + accounts = "accounts", + companies = "companies", + nace = "nace" +} + +export type Mutation = { + __typename?: "Mutation", + updateMe:AccountModel, + updateMeEmail:AccountModel, + updateMePassword:AccountModel, + resetPassword:SimpleResult, + registerGuest:AccountModel, + accountsAddOne:AccountModel, + accountsEditOne:AccountModel, + accountsDeleteOne:AccountModel, + completeRegistration:AccountModel, + categoriesAddOne:CategoryModel, + categoriesEditOne:CategoryModel, + categoriesDeleteOne:CategoryModel, + emailsSettingsAddOne:EmailSettingsModel, + emailsSettingsEditOne:EmailSettingsModel, + emailsSettingsDeleteOne:EmailSettingsModel, + projectSettingsAddOne:ProjectSettingModel, + projectSettingsEditOne:ProjectSettingModel, + projectSettingsDeleteOne:ProjectSettingModel, + listsAddOne:ListModel, + listsEditOne:ListModel, + listsDeleteOne:ListModel, + pagesAddOne:PageSchema, + pagesEditOne:PageSchema, + pagesDeleteOne:PageSchema, + refreshCaching:SuccessResponse, + naceAddOne:NaceSchema, + naceEditOne:NaceSchema, + naceDeleteOne:NaceSchema +} + +export type NaceEditInputSchema = { + name:TranslatableInput, + code:string, + macroSelector:string, + description:TranslatableInput, + level:LevelEnum +} + +export type NaceNewInputSchema = { + name:TranslatableInput, + code:string, + macroSelector:string, + description:TranslatableInput, + level:LevelEnum +} + +export type NaceSchema = { + __typename?: "NaceSchema", + name:TranslatableComponent, + code:string, + macroSelector:string, + description:TranslatableComponent, + level:LevelEnum, + _id:string, + organisationId?:string, + paths?:EnginePathComponent[], + by?:MetaBy, + permissions?:MetaPermissions, + createdAt:DateTime, + updatedAt:DateTime +} + +export type NewAccountInputAdmin = { + type:AccountTypeEnum, + organisationIds?:string[], + email:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ImageInput, + phoneNumber?:string, + gender?:AccountGenderEnum, + birthDate?:DateTime, + address?:AddressInput, + language?:AvailableTranslation, + settings?:SettingsInput[] +} + +export type NewCategoryInput = { + ressourceType:RessourceEnum, + title:TranslatableInput, + content?:TranslatableInput, + colorCode?:string +} + +export type NewEmailInput = { + newEmail:string +} + +export type NewEmailSettingsInput = { + subject?:TranslatableInput, + body?:TranslatableInput, + type?:string, + key:string, + custom?:boolean, + templateId?:TranslatableInput, + fromEmail:string, + fromName:string, + replyToEmail:string, + cci?:string[], + smsSettings?:SmsSettingsComponentInput, + slackSettings?:SlackSettingsComponentInput +} + +export type NewListInput = { + title:TranslatableInput, + teaser?:TranslatableInput, + cover?:ImageInput, + thumbnail?:ImageInput, + content?:TranslatableInput, + seo?:SEOInput, + urls?:TranslatableInput, + ressourceType:ListEnum, + colorCode?:string +} + +export type NewPageInput = { + title:string, + label?:string, + content?:JSONObject, + position?:number, + toComplete?:boolean +} + +export type NewPasswordInput = { + oldPassword:string, + newPassword:string, + newPasswordConfirmation:string +} + +export type NewProjectSettingsInput = { + key:string, + value:string +} + +export type PageSchema = { + __typename?: "PageSchema", + title:string, + label?:string, + content?:JSONObject, + position?:number, + toComplete?:boolean, + _id:string, + organisationId?:string, + paths?:EnginePathComponent[], + by?:MetaBy, + permissions?:MetaPermissions, + createdAt:DateTime, + updatedAt:DateTime +} + +export type ProjectSettingModel = { + __typename?: "ProjectSettingModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + type:SettingsType, + key:string, + value:string +} + +export type PushNotifComponent = { + __typename?: "PushNotifComponent", + subject?:TranslatableComponent, + text:TranslatableComponent +} + +export type Query = { + __typename?: "Query", + me:AccountModel, + login:FirebaseTokenResult, + magicLink:SimpleResult, + accountsGetOne:AccountModel, + accountsGetMany:AccountModel[], + categoriesGetOne:CategoryModel, + categoriesGetMany:CategoryModel[], + categoriesGetCount:number, + emailsSettingsGetOne:EmailSettingsModel, + emailsSettingsGetMany:EmailSettingsModel[], + emailsSettingsGetCount:number, + projectSettingsGetOne:ProjectSettingModel, + projectSettingsGetMany:ProjectSettingModel[], + projectSettingsGetCount:number, + listsGetOne:ListModel, + listsGetMany:ListModel[], + listsGetCount:number, + pagesGetOne:PageSchema, + pagesGetMany:PageSchema[], + pagesGetCount:number, + naceGetOne:NaceSchema, + naceGetMany:NaceSchema[], + naceGetCount:number +} + +export type ResetPasswordInput = { + email:string +} + +export enum RessourceEnum { + none = "none" +} + +export type SEOField = { + __typename?: "SEOField", + title:TranslatableComponent, + description:TranslatableComponent, + keywords?:TranslatableComponent, + thumbnail?:ImageComponent +} + +export type SEOInput = { + title:TranslatableInput, + description:TranslatableInput, + keywords?:TranslatableInput, + thumbnail?:ImageInput +} + +export type SettingsComponent = { + __typename?: "SettingsComponent", + type:SettingsTypeEnum, + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +} + +export type SettingsInput = { + type:SettingsTypeEnum, + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +} + +export enum SettingsType { + emails = "emails", + ac = "ac", + env = "env", + instagram = "instagram", + others = "others" +} + +export enum SettingsTypeEnum { + notification = "notification" +} + +export type SimpleResult = { + __typename?: "SimpleResult", + message:string +} + +export type SlackActionInfo = { + __typename?: "SlackActionInfo", + text:string, + url:string, + style?:SlackActionStyleEnum +} + +export type SlackActionInfoInput = { + text:string, + url:string, + style?:SlackActionStyleEnum +} + +export enum SlackActionStyleEnum { + primary = "primary", + danger = "danger" +} + +export type SlackSettingsComponent = { + __typename?: "SlackSettingsComponent", + webhookUrls:string[], + username?:string, + text:string, + icon?:string, + actions?:SlackActionInfo[] +} + +export type SlackSettingsComponentInput = { + webhookUrls:string[], + username?:string, + text:string, + icon?:string, + actions?:SlackActionInfoInput[] +} + +export type SmsSettingsComponent = { + __typename?: "SmsSettingsComponent", + text:TranslatableComponent +} + +export type SmsSettingsComponentInput = { + text:TranslatableInput +} + +export type SuccessResponse = { + __typename?: "SuccessResponse", + success:boolean +} + +export type TranslatableComponent = { + __typename?: "TranslatableComponent", + en?:string +} + +export type TranslatableInput = { + en?:string +} + +export const AllTypesProps: Record = { + AccountGenderEnum: "enum", + AccountTypeEnum: "enum", + AddressInput:{ + number:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + street:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + streetBis:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + floor:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + box:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + zip:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + state:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + city:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + country:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + AvailableTranslation: "enum", + CompleteAccountInput:{ + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + organisation:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + position:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + DateTime: "String", + EditAccountInput:{ + userName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + profilePicture:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + phoneNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + gender:{ + type:"AccountGenderEnum", + array:false, + arrayRequired:false, + required:false + }, + birthDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:false + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + settings:{ + type:"SettingsInput", + array:true, + arrayRequired:false, + required:true + }, + organisation:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + position:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + EditCategoryInput:{ + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + title:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + content:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + colorCode:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + EditEmailSettingsInput:{ + subject:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + body:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + type:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + custom:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + templateId:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + fromEmail:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + fromName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + replyToEmail:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + cci:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + smsSettings:{ + type:"SmsSettingsComponentInput", + array:false, + arrayRequired:false, + required:false + }, + slackSettings:{ + type:"SlackSettingsComponentInput", + array:false, + arrayRequired:false, + required:false + } + }, + EditListInput:{ + title:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + teaser:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + cover:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + content:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + seo:{ + type:"SEOInput", + array:false, + arrayRequired:false, + required:false + }, + urls:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:false + }, + colorCode:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + EditPageInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + label:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + content:{ + type:"JSONObject", + array:false, + arrayRequired:false, + required:false + }, + position:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + toComplete:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + EditProjectSettingsInput:{ + value:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + GetArgs:{ + limit:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + skip:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + sort:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + ImageInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + fileType:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + large:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + medium:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + small:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + JSONObject: "String", + LevelEnum: "enum", + LinkEmailInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + idToken:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + ListEnum: "enum", + LoginInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + ModelLoadersEnum: "enum", + Mutation:{ + updateMe:{ + input:{ + type:"EditAccountInput", + array:false, + arrayRequired:false, + required:true + } + }, + updateMeEmail:{ + input:{ + type:"NewEmailInput", + array:false, + arrayRequired:false, + required:true + } + }, + updateMePassword:{ + input:{ + type:"NewPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + resetPassword:{ + input:{ + type:"ResetPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + registerGuest:{ + otherInfo:{ + type:"EditAccountInput", + array:false, + arrayRequired:false, + required:true + }, + input:{ + type:"LinkEmailInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsAddOne:{ + input:{ + type:"NewAccountInputAdmin", + array:false, + arrayRequired:false, + required:true + } + }, + accountsEditOne:{ + input:{ + type:"EditAccountInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsDeleteOne:{ + userType:{ + type:"AccountTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + completeRegistration:{ + input:{ + type:"CompleteAccountInput", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesAddOne:{ + input:{ + type:"NewCategoryInput", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesEditOne:{ + input:{ + type:"EditCategoryInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + emailsSettingsAddOne:{ + input:{ + type:"NewEmailSettingsInput", + array:false, + arrayRequired:false, + required:true + } + }, + emailsSettingsEditOne:{ + input:{ + type:"EditEmailSettingsInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + emailsSettingsDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + projectSettingsAddOne:{ + input:{ + type:"NewProjectSettingsInput", + array:false, + arrayRequired:false, + required:true + } + }, + projectSettingsEditOne:{ + input:{ + type:"EditProjectSettingsInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + projectSettingsDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + listsAddOne:{ + input:{ + type:"NewListInput", + array:false, + arrayRequired:false, + required:true + } + }, + listsEditOne:{ + input:{ + type:"EditListInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + listsDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + pagesAddOne:{ + input:{ + type:"NewPageInput", + array:false, + arrayRequired:false, + required:true + } + }, + pagesEditOne:{ + input:{ + type:"EditPageInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + pagesDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + naceAddOne:{ + input:{ + type:"NaceNewInputSchema", + array:false, + arrayRequired:false, + required:true + } + }, + naceEditOne:{ + input:{ + type:"NaceEditInputSchema", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + naceDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + } + }, + NaceEditInputSchema:{ + name:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + code:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + macroSelector:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + description:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + level:{ + type:"LevelEnum", + array:false, + arrayRequired:false, + required:true + } + }, + NaceNewInputSchema:{ + name:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + code:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + macroSelector:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + description:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + level:{ + type:"LevelEnum", + array:false, + arrayRequired:false, + required:true + } + }, + NewAccountInputAdmin:{ + type:{ + type:"AccountTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + organisationIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + userName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + profilePicture:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + phoneNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + gender:{ + type:"AccountGenderEnum", + array:false, + arrayRequired:false, + required:false + }, + birthDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:false + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + settings:{ + type:"SettingsInput", + array:true, + arrayRequired:false, + required:true + } + }, + NewCategoryInput:{ + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + title:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + content:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + colorCode:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + NewEmailInput:{ + newEmail:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + NewEmailSettingsInput:{ + subject:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + body:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + type:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + key:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + custom:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + templateId:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + fromEmail:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + fromName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + replyToEmail:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + cci:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + smsSettings:{ + type:"SmsSettingsComponentInput", + array:false, + arrayRequired:false, + required:false + }, + slackSettings:{ + type:"SlackSettingsComponentInput", + array:false, + arrayRequired:false, + required:false + } + }, + NewListInput:{ + title:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + teaser:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + cover:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + content:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + seo:{ + type:"SEOInput", + array:false, + arrayRequired:false, + required:false + }, + urls:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:true + }, + colorCode:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + NewPageInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + label:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + content:{ + type:"JSONObject", + array:false, + arrayRequired:false, + required:false + }, + position:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + toComplete:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + NewPasswordInput:{ + oldPassword:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + newPassword:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + newPasswordConfirmation:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + NewProjectSettingsInput:{ + key:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + value:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + Query:{ + login:{ + refreshToken:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + creds:{ + type:"LoginInput", + array:false, + arrayRequired:false, + required:false + } + }, + magicLink:{ + input:{ + type:"ResetPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsGetMany:{ + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + categoriesGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesGetMany:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + categoriesGetCount:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + emailsSettingsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + emailsSettingsGetMany:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + emailsSettingsGetCount:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + projectSettingsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + projectSettingsGetMany:{ + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + type:{ + type:"SettingsType", + array:false, + arrayRequired:false, + required:false + } + }, + projectSettingsGetCount:{ + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + type:{ + type:"SettingsType", + array:false, + arrayRequired:false, + required:false + } + }, + listsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + listsGetMany:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + listsGetCount:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + pagesGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + pagesGetMany:{ + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + pagesGetCount:{ + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + naceGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + naceGetMany:{ + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + name:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + code:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + macroSelector:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + level:{ + type:"LevelEnum", + array:false, + arrayRequired:false, + required:false + } + }, + naceGetCount:{ + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + name:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + code:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + macroSelector:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + level:{ + type:"LevelEnum", + array:false, + arrayRequired:false, + required:false + } + } + }, + ResetPasswordInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + RessourceEnum: "enum", + SEOInput:{ + title:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + description:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + }, + keywords:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + } + }, + SettingsInput:{ + type:{ + type:"SettingsTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + email:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + pushNotifications:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + sms:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + SettingsType: "enum", + SettingsTypeEnum: "enum", + SlackActionInfoInput:{ + text:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + url:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + style:{ + type:"SlackActionStyleEnum", + array:false, + arrayRequired:false, + required:false + } + }, + SlackActionStyleEnum: "enum", + SlackSettingsComponentInput:{ + webhookUrls:{ + type:"String", + array:true, + arrayRequired:true, + required:true + }, + username:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + text:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + icon:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + actions:{ + type:"SlackActionInfoInput", + array:true, + arrayRequired:false, + required:true + } + }, + SmsSettingsComponentInput:{ + text:{ + type:"TranslatableInput", + array:false, + arrayRequired:false, + required:true + } + }, + TranslatableInput:{ + en:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + } +} + +export const ReturnTypes: Record = { + AccountModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + types:"AccountTypeEnum", + organisationIds:"String", + email:"String", + userName:"String", + firstName:"String", + lastName:"String", + profilePicture:"ImageComponent", + phoneNumber:"String", + gender:"AccountGenderEnum", + birthDate:"DateTime", + getAge:"Int", + address:"AddressComponent", + language:"AvailableTranslation", + settings:"SettingsComponent", + organisation:"String", + position:"String", + profileCompleted:"Boolean" + }, + AddressComponent:{ + number:"String", + street:"String", + streetBis:"String", + floor:"String", + box:"String", + zip:"String", + state:"String", + city:"String", + country:"String" + }, + CategoryModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"TranslatableComponent", + teaser:"TranslatableComponent", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"TranslatableComponent", + seo:"SEOField", + urls:"TranslatableComponent", + ressourceType:"RessourceEnum", + colorCode:"String" + }, + EmailSettingsModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + type:"SettingsType", + key:"String", + subject:"TranslatableComponent", + body:"TranslatableComponent", + custom:"Boolean", + templateId:"TranslatableComponent", + fromEmail:"String", + fromName:"String", + replyToEmail:"String", + cci:"String", + availableFields:"String", + templateSample:"String", + smsSettings:"SmsSettingsComponent", + pushSettings:"PushNotifComponent", + slackSettings:"SlackSettingsComponent" + }, + EnginePathComponent:{ + ressourceModel:"ModelLoadersEnum", + ressourceId:"String" + }, + FirebaseTokenResult:{ + localId:"String", + email:"String", + displayName:"String", + idToken:"String", + registered:"Boolean", + refreshToken:"String", + expiresIn:"String" + }, + IEngineSchema:{ + "...on NaceSchema": "NaceSchema", + "...on PageSchema": "PageSchema", + _id:"ID", + organisationId:"ID", + paths:"EnginePathComponent", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime" + }, + ImageComponent:{ + title:"String", + fileType:"String", + large:"String", + medium:"String", + small:"String" + }, + ListModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"TranslatableComponent", + teaser:"TranslatableComponent", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"TranslatableComponent", + seo:"SEOField", + urls:"TranslatableComponent", + ressourceType:"ListEnum", + colorCode:"String" + }, + MetaBy:{ + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID" + }, + MetaPermissions:{ + r:"String", + w:"String", + d:"String" + }, + Mutation:{ + updateMe:"AccountModel", + updateMeEmail:"AccountModel", + updateMePassword:"AccountModel", + resetPassword:"SimpleResult", + registerGuest:"AccountModel", + accountsAddOne:"AccountModel", + accountsEditOne:"AccountModel", + accountsDeleteOne:"AccountModel", + completeRegistration:"AccountModel", + categoriesAddOne:"CategoryModel", + categoriesEditOne:"CategoryModel", + categoriesDeleteOne:"CategoryModel", + emailsSettingsAddOne:"EmailSettingsModel", + emailsSettingsEditOne:"EmailSettingsModel", + emailsSettingsDeleteOne:"EmailSettingsModel", + projectSettingsAddOne:"ProjectSettingModel", + projectSettingsEditOne:"ProjectSettingModel", + projectSettingsDeleteOne:"ProjectSettingModel", + listsAddOne:"ListModel", + listsEditOne:"ListModel", + listsDeleteOne:"ListModel", + pagesAddOne:"PageSchema", + pagesEditOne:"PageSchema", + pagesDeleteOne:"PageSchema", + refreshCaching:"SuccessResponse", + naceAddOne:"NaceSchema", + naceEditOne:"NaceSchema", + naceDeleteOne:"NaceSchema" + }, + NaceSchema:{ + name:"TranslatableComponent", + code:"String", + macroSelector:"String", + description:"TranslatableComponent", + level:"LevelEnum", + _id:"ID", + organisationId:"ID", + paths:"EnginePathComponent", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime" + }, + PageSchema:{ + title:"String", + label:"String", + content:"JSONObject", + position:"Int", + toComplete:"Boolean", + _id:"ID", + organisationId:"ID", + paths:"EnginePathComponent", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime" + }, + ProjectSettingModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + type:"SettingsType", + key:"String", + value:"String" + }, + PushNotifComponent:{ + subject:"TranslatableComponent", + text:"TranslatableComponent" + }, + Query:{ + me:"AccountModel", + login:"FirebaseTokenResult", + magicLink:"SimpleResult", + accountsGetOne:"AccountModel", + accountsGetMany:"AccountModel", + categoriesGetOne:"CategoryModel", + categoriesGetMany:"CategoryModel", + categoriesGetCount:"Float", + emailsSettingsGetOne:"EmailSettingsModel", + emailsSettingsGetMany:"EmailSettingsModel", + emailsSettingsGetCount:"Float", + projectSettingsGetOne:"ProjectSettingModel", + projectSettingsGetMany:"ProjectSettingModel", + projectSettingsGetCount:"Float", + listsGetOne:"ListModel", + listsGetMany:"ListModel", + listsGetCount:"Float", + pagesGetOne:"PageSchema", + pagesGetMany:"PageSchema", + pagesGetCount:"Float", + naceGetOne:"NaceSchema", + naceGetMany:"NaceSchema", + naceGetCount:"Float" + }, + SEOField:{ + title:"TranslatableComponent", + description:"TranslatableComponent", + keywords:"TranslatableComponent", + thumbnail:"ImageComponent" + }, + SettingsComponent:{ + type:"SettingsTypeEnum", + email:"Boolean", + pushNotifications:"Boolean", + sms:"Boolean" + }, + SimpleResult:{ + message:"String" + }, + SlackActionInfo:{ + text:"String", + url:"String", + style:"SlackActionStyleEnum" + }, + SlackSettingsComponent:{ + webhookUrls:"String", + username:"String", + text:"String", + icon:"String", + actions:"SlackActionInfo" + }, + SmsSettingsComponent:{ + text:"TranslatableComponent" + }, + SuccessResponse:{ + success:"Boolean" + }, + TranslatableComponent:{ + en:"String" + } +} + +export class GraphQLError extends Error { + constructor(public response: GraphQLResponse) { + super(""); + console.error(response); + } + toString() { + return "GraphQL Response Error"; + } + } + + + +export type UnwrapPromise = T extends Promise ? R : T; +export type ZeusState Promise> = NonNullable< + UnwrapPromise> +>; +export type ZeusHook< + T extends ( + ...args: any[] + ) => Record Promise>, + N extends keyof ReturnType +> = ZeusState[N]>; + +type Func

= (...args: P) => R; +type AnyFunc = Func; + +type WithTypeNameValue = T & { + __typename?: true; +}; + +type AliasType = WithTypeNameValue & { + __alias?: Record>; +}; + +type NotUndefined = T extends undefined ? never : T; + +export type ResolverType = NotUndefined; + +export type ArgsType = F extends Func ? P : never; + +interface GraphQLResponse { + data?: Record; + errors?: Array<{ + message: string; + }>; +} + +export type ValuesOf = T[keyof T]; + +export type MapResolve = SRC extends { + __interface: infer INTERFACE; + __resolve: Record & infer IMPLEMENTORS; + } + ? + ValuesOf<{ + [k in (keyof SRC['__resolve'] & keyof DST)]: ({ + [rk in (keyof SRC['__resolve'][k] & keyof DST[k])]: LastMapTypeSRCResolver + } & { + __typename: SRC['__resolve'][k]['__typename'] + }) + }> + : + never; + +export type MapInterface = SRC extends { + __interface: infer INTERFACE; + __resolve: Record & infer IMPLEMENTORS; + } + ? + (MapResolve extends never ? {} : MapResolve) & { + [k in (keyof SRC['__interface'] & keyof DST)]: LastMapTypeSRCResolver +} : never; + +export type ValueToUnion = T extends { + __typename: infer R; +} + ? { + [P in keyof Omit]: T[P] & { + __typename: R; + }; + } + : T; + +export type ObjectToUnion = { + [P in keyof T]: T[P]; +}[keyof T]; + +type Anify = { [P in keyof T]?: any }; + + +type LastMapTypeSRCResolver = SRC extends undefined + ? undefined + : SRC extends Array + ? LastMapTypeSRCResolver[] + : SRC extends { __interface: any; __resolve: any } + ? MapInterface + : SRC extends { __union: any; __resolve: infer RESOLVE } + ? ObjectToUnion>> + : DST extends boolean + ? SRC + : MapType; + +export type MapType, DST> = DST extends boolean + ? SRC + : DST extends { + __alias: any; + } + ? { + [A in keyof DST["__alias"]]: Required extends Anify< + DST["__alias"][A] + > + ? MapType, DST["__alias"][A]> + : never; + } & + { + [Key in keyof Omit]: DST[Key] extends [ + any, + infer PAYLOAD + ] + ? LastMapTypeSRCResolver + : LastMapTypeSRCResolver; + } + : { + [Key in keyof DST]: DST[Key] extends [any, infer PAYLOAD] + ? LastMapTypeSRCResolver + : LastMapTypeSRCResolver; + }; + +type OperationToGraphQL = (o: Z | V, variables?: Record) => Promise>; + +type CastToGraphQL = ( + resultOfYourQuery: any +) => (o: Z | V) => MapType; + +type fetchOptions = ArgsType; + +export type SelectionFunction = (t: T | V) => T; +type FetchFunction = (query: string, variables?: Record) => Promise; + + + +export const ZeusSelect = () => ((t: any) => t) as SelectionFunction; + +export const ScalarResolver = (scalar: string, value: any) => { + switch (scalar) { + case 'String': + return `${JSON.stringify(value)}`; + case 'Int': + return `${value}`; + case 'Float': + return `${value}`; + case 'Boolean': + return `${value}`; + case 'ID': + return `"${value}"`; + case 'enum': + return `${value}`; + case 'scalar': + return `${value}`; + default: + return false; + } +}; + + +export const TypesPropsResolver = ({ + value, + type, + name, + key, + blockArrays +}: { + value: any; + type: string; + name: string; + key?: string; + blockArrays?: boolean; +}): string => { + if (value === null) { + return `null`; + } + let resolvedValue = AllTypesProps[type][name]; + if (key) { + resolvedValue = resolvedValue[key]; + } + if (!resolvedValue) { + throw new Error(`Cannot resolve ${type} ${name}${key ? ` ${key}` : ''}`) + } + const typeResolved = resolvedValue.type; + const isArray = resolvedValue.array; + const isArrayRequired = resolvedValue.arrayRequired; + if (typeof value === 'string' && value.startsWith(`ZEUS_VAR$`)) { + const isRequired = resolvedValue.required ? '!' : ''; + let t = `${typeResolved}`; + if (isArray) { + if (isRequired) { + t = `${t}!`; + } + t = `[${t}]`; + if(isArrayRequired){ + t = `${t}!`; + } + }else{ + if (isRequired) { + t = `${t}!`; + } + } + return `\$${value.split(`ZEUS_VAR$`)[1]}__ZEUS_VAR__${t}`; + } + if (isArray && !blockArrays) { + return `[${value + .map((v: any) => TypesPropsResolver({ value: v, type, name, key, blockArrays: true })) + .join(',')}]`; + } + const reslovedScalar = ScalarResolver(typeResolved, value); + if (!reslovedScalar) { + const resolvedType = AllTypesProps[typeResolved]; + if (typeof resolvedType === 'object') { + const argsKeys = Object.keys(resolvedType); + return `{${argsKeys + .filter((ak) => value[ak] !== undefined) + .map( + (ak) => `${ak}:${TypesPropsResolver({ value: value[ak], type: typeResolved, name: ak })}` + )}}`; + } + return ScalarResolver(AllTypesProps[typeResolved], value) as string; + } + return reslovedScalar; +}; + + +const isArrayFunction = ( + parent: string[], + a: any[] +) => { + const [values, r] = a; + const [mainKey, key, ...keys] = parent; + const keyValues = Object.keys(values).filter((k) => typeof values[k] !== 'undefined'); + + if (!keys.length) { + return keyValues.length > 0 + ? `(${keyValues + .map( + (v) => + `${v}:${TypesPropsResolver({ + value: values[v], + type: mainKey, + name: key, + key: v + })}` + ) + .join(',')})${r ? traverseToSeekArrays(parent, r) : ''}` + : traverseToSeekArrays(parent, r); + } + + const [typeResolverKey] = keys.splice(keys.length - 1, 1); + let valueToResolve = ReturnTypes[mainKey][key]; + for (const k of keys) { + valueToResolve = ReturnTypes[valueToResolve][k]; + } + + const argumentString = + keyValues.length > 0 + ? `(${keyValues + .map( + (v) => + `${v}:${TypesPropsResolver({ + value: values[v], + type: valueToResolve, + name: typeResolverKey, + key: v + })}` + ) + .join(',')})${r ? traverseToSeekArrays(parent, r) : ''}` + : traverseToSeekArrays(parent, r); + return argumentString; +}; + + +const resolveKV = (k: string, v: boolean | string | { [x: string]: boolean | string }) => + typeof v === 'boolean' ? k : typeof v === 'object' ? `${k}{${objectToTree(v)}}` : `${k}${v}`; + + +const objectToTree = (o: { [x: string]: boolean | string }): string => + `{${Object.keys(o).map((k) => `${resolveKV(k, o[k])}`).join(' ')}}`; + + +const traverseToSeekArrays = (parent: string[], a?: any): string => { + if (!a) return ''; + if (Object.keys(a).length === 0) { + return ''; + } + let b: Record = {}; + if (Array.isArray(a)) { + return isArrayFunction([...parent], a); + } else { + if (typeof a === 'object') { + Object.keys(a) + .filter((k) => typeof a[k] !== 'undefined') + .map((k) => { + if (k === '__alias') { + Object.keys(a[k]).map((aliasKey) => { + const aliasOperations = a[k][aliasKey]; + const aliasOperationName = Object.keys(aliasOperations)[0]; + const aliasOperation = aliasOperations[aliasOperationName]; + b[ + `${aliasOperationName}__alias__${aliasKey}: ${aliasOperationName}` + ] = traverseToSeekArrays([...parent, aliasOperationName], aliasOperation); + }); + } else { + b[k] = traverseToSeekArrays([...parent, k], a[k]); + } + }); + } else { + return ''; + } + } + return objectToTree(b); +}; + + +const buildQuery = (type: string, a?: Record) => + traverseToSeekArrays([type], a); + + +const inspectVariables = (query: string) => { + const regex = /\$\b\w*__ZEUS_VAR__\[?[^!^\]^\s^,^\)^\}]*[!]?[\]]?[!]?/g; + let result; + const AllVariables: string[] = []; + while ((result = regex.exec(query))) { + if (AllVariables.includes(result[0])) { + continue; + } + AllVariables.push(result[0]); + } + if (!AllVariables.length) { + return query; + } + let filteredQuery = query; + AllVariables.forEach((variable) => { + while (filteredQuery.includes(variable)) { + filteredQuery = filteredQuery.replace(variable, variable.split('__ZEUS_VAR__')[0]); + } + }); + return `(${AllVariables.map((a) => a.split('__ZEUS_VAR__')) + .map(([variableName, variableType]) => `${variableName}:${variableType}`) + .join(', ')})${filteredQuery}`; +}; + + +const queryConstruct = (t: 'query' | 'mutation' | 'subscription', tName: string) => (o: Record) => + `${t.toLowerCase()}${inspectVariables(buildQuery(tName, o))}`; + + +const fullChainConstruct = (fn: FetchFunction) => (t: 'query' | 'mutation' | 'subscription', tName: string) => ( + o: Record, + variables?: Record, +) => fn(queryConstruct(t, tName)(o), variables).then((r:any) => { + seekForAliases(r) + return r +}); + + +const seekForAliases = (response: any) => { + const traverseAlias = (value: any) => { + if (Array.isArray(value)) { + value.forEach(seekForAliases); + } else { + if (typeof value === 'object') { + seekForAliases(value); + } + } + }; + if (typeof response === 'object' && response) { + const keys = Object.keys(response); + if (keys.length < 1) { + return; + } + keys.forEach((k) => { + const value = response[k]; + if (k.indexOf('__alias__') !== -1) { + const [operation, alias] = k.split('__alias__'); + response[alias] = { + [operation]: value, + }; + delete response[k]; + } + traverseAlias(value); + }); + } +}; + + +export const $ = (t: TemplateStringsArray): any => `ZEUS_VAR$${t.join('')}`; + + +const handleFetchResponse = ( + response: Parameters['then']>[0], Function>>[0] +): Promise => { + if (!response.ok) { + return new Promise((_, reject) => { + response.text().then(text => { + try { reject(JSON.parse(text)); } + catch (err) { reject(text); } + }).catch(reject); + }); + } + return response.json(); +}; + +const apiFetch = (options: fetchOptions) => (query: string, variables: Record = {}) => { + let fetchFunction = fetch; + let queryString = query; + let fetchOptions = options[1] || {}; + if (fetchOptions.method && fetchOptions.method === 'GET') { + queryString = encodeURIComponent(query); + return fetchFunction(`${options[0]}?query=${queryString}`, fetchOptions) + .then(handleFetchResponse) + .then((response: GraphQLResponse) => { + if (response.errors) { + throw new GraphQLError(response); + } + return response.data; + }); + } + return fetchFunction(`${options[0]}`, { + body: JSON.stringify({ query: queryString, variables }), + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + ...fetchOptions + }) + .then(handleFetchResponse) + .then((response: GraphQLResponse) => { + if (response.errors) { + throw new GraphQLError(response); + } + return response.data; + }); + }; + + + +export const Thunder = (fn: FetchFunction) => ({ + query: ((o: any, variables) => + fullChainConstruct(fn)('query', 'Query')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL, +mutation: ((o: any, variables) => + fullChainConstruct(fn)('mutation', 'Mutation')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL +}); + +export const Chain = (...options: fetchOptions) => ({ + query: ((o: any, variables) => + fullChainConstruct(apiFetch(options))('query', 'Query')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL, +mutation: ((o: any, variables) => + fullChainConstruct(apiFetch(options))('mutation', 'Mutation')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL +}); +export const Zeus = { + query: (o:ValueTypes["Query"]) => queryConstruct('query', 'Query')(o), +mutation: (o:ValueTypes["Mutation"]) => queryConstruct('mutation', 'Mutation')(o) +}; +export const Cast = { + query: ((o: any) => (_: any) => o) as CastToGraphQL< + ValueTypes["Query"], + Query +>, +mutation: ((o: any) => (_: any) => o) as CastToGraphQL< + ValueTypes["Mutation"], + Mutation +> +}; +export const Selectors = { + query: ZeusSelect(), +mutation: ZeusSelect() +}; + + +export const Gql = Chain('http://localhost:4001') \ No newline at end of file diff --git a/__tests/admin/index.ts b/__tests/admin/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/__tests/app/config.ts b/__tests/app/config.ts new file mode 100644 index 0000000..54d0d02 --- /dev/null +++ b/__tests/app/config.ts @@ -0,0 +1,7 @@ +import { Thunder } from './helpers/graphql-zeus'; +import { testRunQuery } from '../../lib/seed/__tests/helper'; + +const url = 'http://localhost:4000'; +const token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjRlMDBlOGZlNWYyYzg4Y2YwYzcwNDRmMzA3ZjdlNzM5Nzg4ZTRmMWUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vd29ya2luZ2ZsZXgtcHJvZHVjdGlvbiIsImF1ZCI6IndvcmtpbmdmbGV4LXByb2R1Y3Rpb24iLCJhdXRoX3RpbWUiOjE2MTYxNzE2MTcsInVzZXJfaWQiOiI4TW1EdmZHbWV0Ylc5aVE5S0Iybm55TFRsV3gyIiwic3ViIjoiOE1tRHZmR21ldGJXOWlROUtCMm5ueUxUbFd4MiIsImlhdCI6MTYxNjE3MTYxNywiZXhwIjoxNjE2MTc1MjE3LCJlbWFpbCI6Im5pY29sYXMuYS5iZXJuaWVyK3dvcmtAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsibmljb2xhcy5hLmJlcm5pZXIrd29ya0BnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJjdXN0b20ifX0.JQvWIyuMmh2tWtf6I1ACWadhyC7LbwvBdaqE0wtXy5YX2dj_cdrKyV1H35nmJ4xRXAoTfMIdarnfPqZaCcCDrAMVey4Jt2BU9dQ0COoQ-qz5KGzeFbeUU-MDSx3sP7sEm_qsdcgPvBHPAj2WnBjrUh58Je1JRVORcXbn9oopJ0v-U2sSn1H43Ycv5qDGF9eqPHaz3ex0NmVum77JRPIKGOKPopCDKrJgMG1tlUiSsr2Qo0YgmxuzY5SrIvVImXmyclNMNrEiRsemTACBScX9seDl_vKosbgIPFaTFqtJksI8NeKxBQFCKz78DwZpZPHxwlN7IEP-ilzIbbppFoHFIQ' + +export const thunder = Thunder(testRunQuery(url, token)); diff --git a/__tests/app/helpers/graphql-zeus.ts b/__tests/app/helpers/graphql-zeus.ts new file mode 100644 index 0000000..a046ef4 --- /dev/null +++ b/__tests/app/helpers/graphql-zeus.ts @@ -0,0 +1,6824 @@ +/* tslint:disable */ +/* eslint-disable */ + +export type ValueTypes = { + ["AccountGenderEnum"]:AccountGenderEnum; + ["AccountModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + types?:true, + organisationIds?:true, + email?:true, + userName?:true, + firstName?:true, + lastName?:true, + profilePicture?:ValueTypes["ImageComponent"], + phoneNumber?:true, + gender?:true, + birthDate?:true, + getAge?:true, + address?:ValueTypes["AddressComponent"], + language?:true, + settings?:ValueTypes["SettingsComponent"], + paymentInfo?:ValueTypes["AccountPaymentInfo"], + payoutInfo?:ValueTypes["AccountPayoutInfo"], + terms?:true, + favLikes?:ValueTypes["FavoriteComponent"], + getFavorites?:ValueTypes["FavoriteComponent"], + getLiked?:ValueTypes["FavoriteComponent"], + __typename?: true +}>; + ["AccountPaymentInfo"]: AliasType<{ + stripeInfo?:ValueTypes["StripeInfo"], + paymentMethods?:ValueTypes["PaymentMethodDetail"], + billingInfos?:ValueTypes["BillingAddressComponent"], + __typename?: true +}>; + ["AccountPayoutInfo"]: AliasType<{ + stripeInfo?:ValueTypes["StripePayoutInfo"], + __typename?: true +}>; + ["AccountTypeEnum"]:AccountTypeEnum; + ["AddInput"]: { + ressourceId:string, + ressourceType:ValueTypes["RessourceEnum"] +}; + ["AddOnComponent"]: AliasType<{ + addOnId?:true, + quantity?:true, + __typename?: true +}>; + ["AddPeopleToRoomInput"]: { + roomId:string, + accountIds:string[] +}; + ["AddressComponent"]: AliasType<{ + number?:true, + street?:true, + streetBis?:true, + floor?:true, + box?:true, + zip?:true, + state?:true, + city?:true, + country?:true, + __typename?: true +}>; + ["AddressInput"]: { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +}; + ["AddressStrictComponent"]: AliasType<{ + number?:true, + street?:true, + zip?:true, + city?:true, + country?:true, + streetBis?:true, + floor?:true, + box?:true, + state?:true, + __typename?: true +}>; + ["AddressStrictInput"]: { + number:string, + street:string, + zip:string, + city:string, + country:string, + streetBis?:string, + floor?:string, + box?:string, + state?:string +}; + ["AvailabilityComponent"]: AliasType<{ + dates?:ValueTypes["DateRangeComponent"], + hours?:ValueTypes["HourComponent"], + exceptions?:ValueTypes["DateExceptionComponent"], + noWeekend?:true, + __typename?: true +}>; + ["AvailabilityInput"]: { + dates:ValueTypes["DateRangeComponentInput"], + hours?:ValueTypes["HoursInput"][], + exceptions?:ValueTypes["DateExceptionComponentInput"][], + noWeekend?:boolean +}; + ["AvailableCurrency"]:AvailableCurrency; + ["AvailableTranslation"]:AvailableTranslation; + ["BillingAddressComponent"]: AliasType<{ + firstName?:true, + lastName?:true, + address?:ValueTypes["AddressStrictComponent"], + email?:true, + default?:true, + customTags?:true, + company?:true, + vatNumber?:true, + fiscalForm?:true, + _id?:true, + __typename?: true +}>; + ["BillingAddressInput"]: { + firstName:string, + lastName:string, + address:ValueTypes["AddressStrictInput"], + email?:string, + default?:boolean, + customTags?:ValueTypes["JSONObject"], + company?:string, + vatNumber?:string, + fiscalForm?:string +}; + ["BookingEngineSchema"]: AliasType<{ + durationType?:true, + startToEnd?:ValueTypes["DateRangeComponent"], + slot?:ValueTypes["SlotComponent"], + capacity?:true, + comments?:true, + status?:true, + paths?:ValueTypes["EnginePathComponent"], + ownerId?:true, + dates?:ValueTypes["DateRangeComponent"], + owner?:ValueTypes["AccountModel"], + _id?:true, + organisationId?:true, + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + workspace?:ValueTypes["WorkspaceModel"], + room?:ValueTypes["RoomModel"], + __typename?: true +}>; + ["BookingNewInput"]: AliasType<{ + durationType?:true, + startToEnd?:ValueTypes["DateRangeComponent"], + slot?:ValueTypes["SlotComponent"], + capacity?:true, + comments?:true, + ressourceModel?:true, + ressourceId?:true, + checkoutInfo?:ValueTypes["CheckoutEngineInput"], + __typename?: true +}>; + ["BookingNewInputSchema"]: { + durationType:ValueTypes["DurationTypeEnum"], + startToEnd?:ValueTypes["DateRangeComponentInput"], + slot?:ValueTypes["SlotComponentInput"], + capacity?:number, + comments?:string, + ressourceModel:ValueTypes["BookingsRessourceEnum"], + ressourceId:string, + checkoutInfo?:ValueTypes["CheckoutEngineInputI"] +}; + ["BookingSchema"]: AliasType<{ + durationType?:true, + startToEnd?:ValueTypes["DateRangeComponent"], + slot?:ValueTypes["SlotComponent"], + capacity?:true, + comments?:true, + status?:true, + paths?:ValueTypes["EnginePathComponent"], + ownerId?:true, + dates?:ValueTypes["DateRangeComponent"], + _id?:true, + organisationId?:true, + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + __typename?: true +}>; + ["BookingsRessourceEnum"]:BookingsRessourceEnum; + ["BuyerInfo"]: { + firstName:string, + lastName:string, + email:string, + company?:string, + vatNumber?:string, + address:ValueTypes["AddressInput"] +}; + ["BuyerInfoComponent"]: AliasType<{ + firstName?:true, + lastName?:true, + email?:true, + company?:true, + vatNumber?:true, + address?:ValueTypes["AddressComponent"], + __typename?: true +}>; + ["cardBrandEnum"]:cardBrandEnum; + ["CardInfo"]: AliasType<{ + brand?:true, + country?:true, + exp_month?:true, + exp_year?:true, + last4?:true, + __typename?: true +}>; + ["cardTypeEnum"]:cardTypeEnum; + ["CategoryModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:ValueTypes["TranslatableComponent"], + teaser?:ValueTypes["TranslatableComponent"], + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:ValueTypes["TranslatableComponent"], + seo?:ValueTypes["SEOField"], + urls?:ValueTypes["TranslatableComponent"], + ressourceType?:true, + colorCode?:true, + __typename?: true +}>; + ["CheckoutEngineInput"]: AliasType<{ + contactInfo?:ValueTypes["BuyerInfoComponent"], + billingInfo?:ValueTypes["BuyerInfoComponent"], + token?:true, + __typename?: true +}>; + ["CheckoutEngineInputI"]: { + contactInfo:ValueTypes["BuyerInfo"], + billingInfo:ValueTypes["BuyerInfo"], + token?:string +}; + ["CheckoutInput"]: { + contactInfo:ValueTypes["BuyerInfo"], + billingInfo:ValueTypes["BuyerInfo"], + token?:string +}; + ["CountryCodesComponentEnum"]:CountryCodesComponentEnum; + ["CurrencyEnum"]:CurrencyEnum; + ["DateExceptionComponent"]: AliasType<{ + dates?:ValueTypes["DateRangeComponent"], + hours?:ValueTypes["HourComponent"], + allDay?:true, + __typename?: true +}>; + ["DateExceptionComponentInput"]: { + dates:ValueTypes["DateRangeComponentInput"], + hours?:ValueTypes["HoursInput"][], + allDay?:boolean +}; + ["DateRangeComponent"]: AliasType<{ + startDate?:true, + endDate?:true, + __typename?: true +}>; + ["DateRangeComponentInput"]: { + startDate:ValueTypes["DateTime"], + endDate:ValueTypes["DateTime"] +}; + ["DateTabType"]:DateTabType; + /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +["DateTime"]:unknown; + ["DurationTypeEnum"]:DurationTypeEnum; + ["EditAccountInput"]: { + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ValueTypes["ImageInput"], + phoneNumber?:string, + gender?:ValueTypes["AccountGenderEnum"], + birthDate?:ValueTypes["DateTime"], + address?:ValueTypes["AddressInput"], + language?:ValueTypes["AvailableTranslation"], + settings?:ValueTypes["SettingsInput"][] +}; + ["EditMessageInput"]: { + message:string +}; + ["EditWorkspaceInput"]: { + title?:string, + teaser?:string, + cover?:ValueTypes["ImageInput"], + thumbnail?:ValueTypes["ImageInput"], + extraImages?:ValueTypes["ImageInput"][], + content?:string, + seo?:ValueTypes["SEOSimpleInput"], + urls?:string, + workspaceType?:ValueTypes["WorkspaceTypeEnum"], + pricingPerHour?:number, + currency:ValueTypes["AvailableCurrency"], + maxCapacity?:number, + minStay?:number, + maxStay?:number, + availability?:ValueTypes["AvailabilityInput"], + mainImages:ValueTypes["MainImage"], + equipmentIds?:string[], + featureIds?:string[], + address?:ValueTypes["AddressStrictInput"] +}; + ["EnginePathComponent"]: AliasType<{ + ressourceModel?:true, + ressourceId?:true, + __typename?: true +}>; + ["FavoriteComponent"]: AliasType<{ + ressourceId?:true, + ressourceType?:true, + addedAt?:true, + favorite?:true, + liked?:true, + __typename?: true +}>; + ["FirebaseTokenResult"]: AliasType<{ + localId?:true, + email?:true, + displayName?:true, + idToken?:true, + registered?:true, + refreshToken?:true, + expiresIn?:true, + __typename?: true +}>; + ["GeolocAddressSearchInput"]: { + formattedAddress:string, + radius?:number +}; + ["GeolocDistComponent"]: AliasType<{ + calculated?:true, + location?:ValueTypes["LocComponent"], + __typename?: true +}>; + ["GeolocSearchInput"]: { + longitude:number, + latitude:number, + radius?:number +}; + ["GetArgs"]: { + limit:number, + skip:number, + sort?:string +}; + ["HourComponent"]: AliasType<{ + from?:true, + to?:true, + __typename?: true +}>; + ["HoursInput"]: { + from:string, + to:string +}; + ["IEngineSchema"]:AliasType<{ + _id?:true, + organisationId?:true, + paths?:ValueTypes["EnginePathComponent"], + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true; + ['...on BookingEngineSchema']?: Omit; + ['...on BookingSchema']?: Omit; + ['...on OrderEngineSchema']?: Omit; + __typename?: true +}>; + ["ImageComponent"]: AliasType<{ + title?:true, + fileType?:true, + large?:true, + medium?:true, + small?:true, + __typename?: true +}>; + ["ImageInput"]: { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +}; + ["InvoiceInfo"]: AliasType<{ + provider?:true, + invoiceId?:true, + invoiceNumber?:true, + __typename?: true +}>; + ["InvoicingProvider"]:InvoicingProvider; + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +["JSONObject"]:unknown; + ["LineItemSchema"]: AliasType<{ + _id?:true, + productRessource?:true, + productId?:true, + title?:true, + price?:true, + salesPrice?:true, + quantity?:true, + vatClassId?:true, + parentId?:true, + addOns?:ValueTypes["AddOnComponent"], + localeInfo?:ValueTypes["LocaleInfo"], + promoId?:true, + finalPrice?:true, + getProduct?:ValueTypes["ProductUnion"], + getLinePrice?:true, + getLineVatPrice?:true, + __typename?: true +}>; + ["LinkEmailInput"]: { + email:string, + password:string, + idToken:string +}; + ["ListEnum"]:ListEnum; + ["ListModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:ValueTypes["TranslatableComponent"], + teaser?:ValueTypes["TranslatableComponent"], + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:ValueTypes["TranslatableComponent"], + seo?:ValueTypes["SEOField"], + urls?:ValueTypes["TranslatableComponent"], + ressourceType?:true, + colorCode?:true, + __typename?: true +}>; + ["LocaleInfo"]: AliasType<{ + countryCode?:true, + locale?:true, + __typename?: true +}>; + ["LocComponent"]: AliasType<{ + type?:true, + coordinates?:true, + __typename?: true +}>; + ["LoginInput"]: { + email:string, + password:string +}; + ["MainImage"]: { + main?:ValueTypes["ImageInput"], + one?:ValueTypes["ImageInput"], + two?:ValueTypes["ImageInput"], + three?:ValueTypes["ImageInput"], + four?:ValueTypes["ImageInput"] +}; + ["MainImageComponent"]: AliasType<{ + main?:ValueTypes["ImageComponent"], + one?:ValueTypes["ImageComponent"], + two?:ValueTypes["ImageComponent"], + three?:ValueTypes["ImageComponent"], + four?:ValueTypes["ImageComponent"], + __typename?: true +}>; + ["MessageModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + roomId?:true, + notified?:true, + message?:true, + edited?:true, + deleted?:true, + file?:ValueTypes["ImageComponent"], + sentBy?:true, + readBy?:ValueTypes["MessageReadBy"], + __typename?: true +}>; + ["MessageReadBy"]: AliasType<{ + accountId?:true, + readAt?:true, + __typename?: true +}>; + ["MetaBy"]: AliasType<{ + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + __typename?: true +}>; + ["MetaPermissions"]: AliasType<{ + r?:true, + w?:true, + d?:true, + __typename?: true +}>; + ["ModelLoadersEnum"]:ModelLoadersEnum; + ["Mutation"]: AliasType<{ +updateMe?: [{ input:ValueTypes["EditAccountInput"]},ValueTypes["AccountModel"]], +updateMeEmail?: [{ input:ValueTypes["NewEmailInput"]},ValueTypes["AccountModel"]], +updateMePassword?: [{ input:ValueTypes["NewPasswordInput"]},ValueTypes["AccountModel"]], +resetPassword?: [{ input:ValueTypes["ResetPasswordInput"]},ValueTypes["SimpleResult"]], +registerGuest?: [{ otherInfo:ValueTypes["EditAccountInput"], input:ValueTypes["LinkEmailInput"]},ValueTypes["AccountModel"]], +register?: [{ input:ValueTypes["NewAccountInput"]},ValueTypes["AccountModel"]], +accountsBillingInfosAddOne?: [{ input:ValueTypes["BillingAddressInput"]},ValueTypes["BillingAddressComponent"]], +accountsBillingInfosEditOne?: [{ input:ValueTypes["BillingAddressInput"], id:string},ValueTypes["BillingAddressComponent"]], +accountsBillingInfosDeleteOne?: [{ id:string},true], +accountsPaymentMethodsAddOne?: [{ input:ValueTypes["PaymentMethodInput"]},ValueTypes["PaymentMethodDetail"]], +accountsPaymentMethodsMarkasDefault?: [{ id:string},ValueTypes["PaymentMethodDetail"]], +accountsPaymentMethodsDeleteOne?: [{ id:string},true], + accountsLinkWithStripe?:true, + accountsRefreshInfoStripe?:ValueTypes["AccountModel"], +accountsFavoritesAddOne?: [{ input:ValueTypes["AddInput"]},ValueTypes["FavoriteComponent"]], +accountsFavoritesRemoveOne?: [{ input:ValueTypes["RemoveInput"]},ValueTypes["SuccessResponse"]], +workspacesAddOne?: [{ input:ValueTypes["NewWorkspaceInput"]},ValueTypes["WorkspaceModel"]], +workspacesEditOne?: [{ input:ValueTypes["EditWorkspaceInput"], id:string},ValueTypes["WorkspaceModel"]], +workspacesDeleteOne?: [{ id:string},ValueTypes["WorkspaceModel"]], +finaliseCheckoutWithStripe?: [{ input:ValueTypes["CheckoutInput"]},ValueTypes["OrderEngineSchema"]], + removeCurrentOrder?:ValueTypes["OrderEngineSchema"], +validateOrder?: [{ id:string},ValueTypes["OrderEngineSchema"]], +roomsCreateOne?: [{ input:ValueTypes["NewRoomInput"]},ValueTypes["RoomModel"]], +roomsAddPeople?: [{ input:ValueTypes["AddPeopleToRoomInput"]},ValueTypes["RoomModel"]], +roomsRemovePeople?: [{ input:ValueTypes["RemovePeopleFromRoomInput"]},ValueTypes["RoomModel"]], +roomsMarkAllMessageAsRead?: [{ roomId:string},ValueTypes["RoomModel"]], +reserveOneBooking?: [{ input:ValueTypes["BookingNewInputSchema"]},ValueTypes["OrderEngineSchema"]], +roomsAddOneMessage?: [{ input:ValueTypes["NewMessageInput"]},ValueTypes["MessageModel"]], +roomsEditOneMessage?: [{ input:ValueTypes["EditMessageInput"], id:string},ValueTypes["MessageModel"]], +roomsMarkMessageAsRead?: [{ id:string},ValueTypes["MessageModel"]], +roomsDeleteOneMessage?: [{ id:string},ValueTypes["MessageModel"]], + __typename?: true +}>; + ["NewAccountInput"]: { + email:string, + password:string, + passwordConfirmation:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ValueTypes["ImageInput"], + phoneNumber?:string, + gender?:ValueTypes["AccountGenderEnum"], + birthDate?:ValueTypes["DateTime"], + address?:ValueTypes["AddressInput"], + language?:ValueTypes["AvailableTranslation"], + settings?:ValueTypes["SettingsInput"][], + terms:boolean +}; + ["NewEmailInput"]: { + newEmail:string +}; + ["NewMessageInput"]: { + roomId:string, + message:string, + file?:ValueTypes["ImageInput"] +}; + ["NewPasswordInput"]: { + oldPassword:string, + newPassword:string, + newPasswordConfirmation:string +}; + ["NewRoomInput"]: { + accountIds:string[] +}; + ["NewWorkspaceInput"]: { + title?:string, + teaser?:string, + cover?:ValueTypes["ImageInput"], + thumbnail?:ValueTypes["ImageInput"], + extraImages?:ValueTypes["ImageInput"][], + content?:string, + seo?:ValueTypes["SEOSimpleInput"], + urls?:string, + workspaceType:ValueTypes["WorkspaceTypeEnum"], + pricingPerHour:number, + currency:ValueTypes["AvailableCurrency"], + maxCapacity:number, + minStay:number, + maxStay?:number, + availability:ValueTypes["AvailabilityInput"], + mainImages:ValueTypes["MainImage"], + equipmentIds:string[], + featureIds:string[], + address:ValueTypes["AddressStrictInput"] +}; + ["OrderBusinessStatusEnum"]:OrderBusinessStatusEnum; + ["OrderEngineSchema"]: AliasType<{ + lines?:ValueTypes["LineItemSchema"], + localeInfo?:ValueTypes["LocaleInfo"], + currency?:true, + vatExempt?:true, + accountId?:true, + contactInfo?:ValueTypes["BuyerInfoComponent"], + billingInfo?:ValueTypes["BuyerInfoComponent"], + promoId?:true, + invoiceInfo?:ValueTypes["InvoiceInfo"], + providerOrderItems?:ValueTypes["ProviderSchema"], + orderName?:ValueTypes["TranslatableComponent"], + orderStatus?:true, + paymentIntent?:ValueTypes["PaymentIntentInfo"], + paymentStatus?:true, + paymentInfo?:ValueTypes["PaymentInfo"], + subTotalPrice?:true, + vatPrice?:true, + finalPrice?:true, + promo?:ValueTypes["PromoModel"], + _id?:true, + organisationId?:true, + paths?:ValueTypes["EnginePathComponent"], + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + getBookingInput?:ValueTypes["BookingNewInput"], + __typename?: true +}>; + ["OrderStatusEnum"]:OrderStatusEnum; + ["PaymentInfo"]: AliasType<{ + provider?:true, + transactionId?:true, + __typename?: true +}>; + ["PaymentIntentInfo"]: AliasType<{ + provider?:true, + stripePaymentIntentData?:ValueTypes["StripePaymentIntentData"], + __typename?: true +}>; + ["PaymentMethodDetail"]: AliasType<{ + _id?:true, + sPayMethodId?:true, + default?:true, + nameOnCard?:true, + type?:true, + cardInfo?:ValueTypes["CardInfo"], + __typename?: true +}>; + ["PaymentMethodInput"]: { + sPayMethodId:string, + nameOnCard:string, + default?:boolean +}; + ["PaymentProvider"]:PaymentProvider; + ["PaymentStatusEnum"]:PaymentStatusEnum; + ["PlaceComponent"]: AliasType<{ + placeId?:true, + address?:ValueTypes["AddressComponent"], + loc?:ValueTypes["LocComponent"], + formattedAddress?:true, + __typename?: true +}>; + ["ProductRessourceEnum"]:ProductRessourceEnum; + ["ProductUnion"]: AliasType<{ ["...on WorkspaceModel"] : ValueTypes["WorkspaceModel"] + __typename?: true +}>; + ["PromoModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + code?:true, + description?:ValueTypes["TranslatableComponent"], + type?:true, + value?:true, + validity?:true, + cummulable?:true, + usageLimit?:true, + usage?:true, + __typename?: true +}>; + ["PromoType"]:PromoType; + ["ProviderSchema"]: AliasType<{ + _id?:true, + organisationId?:true, + orderStatus?:true, + lines?:ValueTypes["LineItemSchema"], + __typename?: true +}>; + ["Query"]: AliasType<{ + me?:ValueTypes["AccountModel"], +login?: [{ refreshToken?:string, creds?:ValueTypes["LoginInput"]},ValueTypes["FirebaseTokenResult"]], +magicLink?: [{ input:ValueTypes["ResetPasswordInput"]},ValueTypes["SimpleResult"]], +placesAutocomplete?: [{ session:string, input:string, language?:ValueTypes["AvailableTranslation"], country?:ValueTypes["CountryCodesComponentEnum"][]},true], +placesGeocode?: [{ session:string, placeId:string},true], +placesGeocodeFromAddress?: [{ address:string},true], +categoriesGetOne?: [{ id:string},ValueTypes["CategoryModel"]], +categoriesGetMany?: [{ search?:string, ressourceType:ValueTypes["RessourceEnum"], pagination?:ValueTypes["GetArgs"]},ValueTypes["CategoryModel"]], +categoriesGetCount?: [{ search?:string, ressourceType:ValueTypes["RessourceEnum"], pagination?:ValueTypes["GetArgs"]},true], +listsGetOne?: [{ id:string},ValueTypes["ListModel"]], +listsGetMany?: [{ search?:string, ressourceType:ValueTypes["ListEnum"], pagination?:ValueTypes["GetArgs"]},ValueTypes["ListModel"]], +listsGetCount?: [{ search?:string, ressourceType:ValueTypes["ListEnum"], pagination?:ValueTypes["GetArgs"]},true], + accountsDashboardLinkStripe?:true, +workspacesGetOne?: [{ id:string},ValueTypes["WorkspaceModel"]], +workspacesGetMany?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},ValueTypes["WorkspaceModel"]], +workspacesGetCount?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},true], +workspacesSearchMany?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},ValueTypes["WorkspaceModel"]], +workspacesGetMine?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},ValueTypes["WorkspaceModel"]], +workspacesGetMineCount?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},true], +bookingsGetOne?: [{ id:string},ValueTypes["BookingEngineSchema"]], +bookingsGetMany?: [{ startDate?:ValueTypes["DateTime"], endDate?:ValueTypes["DateTime"], rangeType?:ValueTypes["RangeType"], dateTabType?:ValueTypes["DateTabType"], dateRange?:ValueTypes["DateRangeComponentInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], ressourceModel?:ValueTypes["BookingsRessourceEnum"], ressourceId?:string, status?:ValueTypes["StatusEnum"][]},ValueTypes["BookingEngineSchema"]], +bookingsGetCount?: [{ startDate?:ValueTypes["DateTime"], endDate?:ValueTypes["DateTime"], rangeType?:ValueTypes["RangeType"], dateTabType?:ValueTypes["DateTabType"], dateRange?:ValueTypes["DateRangeComponentInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], ressourceModel?:ValueTypes["BookingsRessourceEnum"], ressourceId?:string, status?:ValueTypes["StatusEnum"][]},true], + myCurrentOrder?:ValueTypes["OrderEngineSchema"], +myOrdersGetOne?: [{ id:string},ValueTypes["OrderEngineSchema"]], +myOrdersGetMany?: [{ orderStatus?:ValueTypes["OrderStatusEnum"], paymentStatus?:ValueTypes["PaymentStatusEnum"], pagination?:ValueTypes["GetArgs"], search?:string},ValueTypes["OrderEngineSchema"]], +myOrdersGetManyCount?: [{ orderStatus?:ValueTypes["OrderStatusEnum"], paymentStatus?:ValueTypes["PaymentStatusEnum"], pagination?:ValueTypes["GetArgs"], search?:string},true], +roomsGetOne?: [{ id:string},ValueTypes["RoomModel"]], +roomsGetMany?: [{ pagination?:ValueTypes["GetArgs"]},ValueTypes["RoomModel"]], +roomsGetMessages?: [{ pagination?:ValueTypes["GetArgs"], id:string},ValueTypes["MessageModel"]], + __typename?: true +}>; + ["RangeType"]:RangeType; + ["RemoveInput"]: { + ressourceId:string +}; + ["RemovePeopleFromRoomInput"]: { + roomId:string, + accountIds:string[] +}; + ["ResetPasswordInput"]: { + email:string +}; + ["RessourceEnum"]:RessourceEnum; + ["RoomModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:true, + stats?:ValueTypes["RoomsStats"], + getAccounts?:ValueTypes["AccountModel"], +getMessages?: [{ pagination?:ValueTypes["GetArgs"]},ValueTypes["MessageModel"]], + lastMessage?:true, + __typename?: true +}>; + ["RoomsStats"]: AliasType<{ + unreadCounts?:ValueTypes["UnreadCount"], + __typename?: true +}>; + ["SEOField"]: AliasType<{ + title?:ValueTypes["TranslatableComponent"], + description?:ValueTypes["TranslatableComponent"], + keywords?:ValueTypes["TranslatableComponent"], + thumbnail?:ValueTypes["ImageComponent"], + __typename?: true +}>; + ["SEOSimpleField"]: AliasType<{ + title?:true, + description?:true, + keywords?:true, + thumbnail?:ValueTypes["ImageComponent"], + __typename?: true +}>; + ["SEOSimpleInput"]: { + title:string, + description:string, + keywords?:string, + thumbnail?:ValueTypes["ImageInput"] +}; + ["SettingsComponent"]: AliasType<{ + type?:true, + email?:true, + pushNotifications?:true, + sms?:true, + __typename?: true +}>; + ["SettingsInput"]: { + type:ValueTypes["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +}; + ["SettingsTypeEnum"]:SettingsTypeEnum; + ["SimpleResult"]: AliasType<{ + message?:true, + __typename?: true +}>; + ["SlotComponent"]: AliasType<{ + startDate?:true, + endDate?:true, + startTime?:true, + endTime?:true, + __typename?: true +}>; + ["SlotComponentInput"]: { + startDate:ValueTypes["DateTime"], + endDate:ValueTypes["DateTime"], + startTime:string, + endTime:string +}; + ["StatusEnum"]:StatusEnum; + ["StripeInfo"]: AliasType<{ + customerId?:true, + __typename?: true +}>; + ["StripePaymentIntentData"]: AliasType<{ + id?:true, + client_secret?:true, + currency?:true, + customer?:true, + status?:true, + __typename?: true +}>; + ["StripePayoutInfo"]: AliasType<{ + accountId?:true, + chargesEnabled?:true, + payoutsEnabled?:true, + detailsSubmitted?:true, + __typename?: true +}>; + ["SuccessResponse"]: AliasType<{ + success?:true, + __typename?: true +}>; + ["TranslatableComponent"]: AliasType<{ + en?:true, + fr?:true, + nl?:true, + de?:true, + __typename?: true +}>; + ["UnreadCount"]: AliasType<{ + accountId?:true, + count?:true, + __typename?: true +}>; + ["WorkspaceModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:true, + teaser?:true, + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:true, + seo?:ValueTypes["SEOSimpleField"], + urls?:true, + place?:ValueTypes["PlaceComponent"], + dist?:ValueTypes["GeolocDistComponent"], +getDistFromLocation?: [{ longitude:number, latitude:number, radius?:number},true], + workspaceType?:true, + pricingPerHour?:true, + currency?:true, + maxCapacity?:true, + minStay?:true, + maxStay?:true, + availability?:ValueTypes["AvailabilityComponent"], + mainImages?:ValueTypes["MainImageComponent"], + equipmentIds?:true, + featureIds?:true, + address?:ValueTypes["AddressComponent"], + finalPrice?:true, + getEquipments?:ValueTypes["ListModel"], + getFeatures?:ValueTypes["ListModel"], +getBookings?: [{ ressourceModel?:ValueTypes["BookingsRessourceEnum"], ressourceId?:string, status?:ValueTypes["StatusEnum"][], startDate?:ValueTypes["DateTime"], endDate?:ValueTypes["DateTime"], rangeType?:ValueTypes["RangeType"], dateTabType?:ValueTypes["DateTabType"], dateRange?:ValueTypes["DateRangeComponentInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"]},ValueTypes["BookingSchema"]], + getOwner?:ValueTypes["WorkspaceOwnerComponent"], + __typename?: true +}>; + ["WorkspaceOwnerComponent"]: AliasType<{ + firstName?:true, + lastName?:true, + __typename?: true +}>; + ["WorkspaceTypeEnum"]:WorkspaceTypeEnum + } + +export type PartialObjects = { + ["AccountGenderEnum"]:AccountGenderEnum, + ["AccountModel"]: { + __typename?: "AccountModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + types?:PartialObjects["AccountTypeEnum"][], + organisationIds?:string[], + email?:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:PartialObjects["ImageComponent"], + phoneNumber?:string, + gender?:PartialObjects["AccountGenderEnum"], + birthDate?:PartialObjects["DateTime"], + getAge?:number, + address?:PartialObjects["AddressComponent"], + language?:PartialObjects["AvailableTranslation"], + settings?:PartialObjects["SettingsComponent"][], + paymentInfo?:PartialObjects["AccountPaymentInfo"], + payoutInfo?:PartialObjects["AccountPayoutInfo"], + terms?:boolean, + favLikes?:(PartialObjects["FavoriteComponent"] | undefined)[], + getFavorites?:PartialObjects["FavoriteComponent"][], + getLiked?:PartialObjects["FavoriteComponent"][] + }, + ["AccountPaymentInfo"]: { + __typename?: "AccountPaymentInfo"; + stripeInfo?:PartialObjects["StripeInfo"], + paymentMethods?:PartialObjects["PaymentMethodDetail"][], + billingInfos?:PartialObjects["BillingAddressComponent"][] + }, + ["AccountPayoutInfo"]: { + __typename?: "AccountPayoutInfo"; + stripeInfo?:PartialObjects["StripePayoutInfo"] + }, + ["AccountTypeEnum"]:AccountTypeEnum, + ["AddInput"]: { + ressourceId:string, + ressourceType:PartialObjects["RessourceEnum"] +}, + ["AddOnComponent"]: { + __typename?: "AddOnComponent"; + addOnId?:string, + quantity?:number + }, + ["AddPeopleToRoomInput"]: { + roomId:string, + accountIds:string[] +}, + ["AddressComponent"]: { + __typename?: "AddressComponent"; + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city?:string, + country?:string + }, + ["AddressInput"]: { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +}, + ["AddressStrictComponent"]: { + __typename?: "AddressStrictComponent"; + number?:string, + street?:string, + zip?:string, + city?:string, + country?:string, + streetBis?:string, + floor?:string, + box?:string, + state?:string + }, + ["AddressStrictInput"]: { + number:string, + street:string, + zip:string, + city:string, + country:string, + streetBis?:string, + floor?:string, + box?:string, + state?:string +}, + ["AvailabilityComponent"]: { + __typename?: "AvailabilityComponent"; + dates?:PartialObjects["DateRangeComponent"], + hours?:PartialObjects["HourComponent"][], + exceptions?:PartialObjects["DateExceptionComponent"][], + noWeekend?:boolean + }, + ["AvailabilityInput"]: { + dates:PartialObjects["DateRangeComponentInput"], + hours?:PartialObjects["HoursInput"][], + exceptions?:PartialObjects["DateExceptionComponentInput"][], + noWeekend?:boolean +}, + ["AvailableCurrency"]:AvailableCurrency, + ["AvailableTranslation"]:AvailableTranslation, + ["BillingAddressComponent"]: { + __typename?: "BillingAddressComponent"; + firstName?:string, + lastName?:string, + address?:PartialObjects["AddressStrictComponent"], + email?:string, + default?:boolean, + customTags?:PartialObjects["JSONObject"], + company?:string, + vatNumber?:string, + fiscalForm?:string, + _id?:string + }, + ["BillingAddressInput"]: { + firstName:string, + lastName:string, + address:PartialObjects["AddressStrictInput"], + email?:string, + default?:boolean, + customTags?:PartialObjects["JSONObject"], + company?:string, + vatNumber?:string, + fiscalForm?:string +}, + ["BookingEngineSchema"]: { + __typename?: "BookingEngineSchema"; + durationType?:PartialObjects["DurationTypeEnum"], + startToEnd?:PartialObjects["DateRangeComponent"], + slot?:PartialObjects["SlotComponent"], + capacity?:number, + comments?:string, + status?:PartialObjects["StatusEnum"], + paths?:PartialObjects["EnginePathComponent"][], + ownerId?:string, + dates?:PartialObjects["DateRangeComponent"][], + owner?:PartialObjects["AccountModel"], + _id?:string, + organisationId?:string, + by?:PartialObjects["MetaBy"], + permissions?:PartialObjects["MetaPermissions"], + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + workspace?:PartialObjects["WorkspaceModel"], + room?:PartialObjects["RoomModel"] + }, + ["BookingNewInput"]: { + __typename?: "BookingNewInput"; + durationType?:PartialObjects["DurationTypeEnum"], + startToEnd?:PartialObjects["DateRangeComponent"], + slot?:PartialObjects["SlotComponent"], + capacity?:number, + comments?:string, + ressourceModel?:PartialObjects["BookingsRessourceEnum"], + ressourceId?:string, + checkoutInfo?:PartialObjects["CheckoutEngineInput"] + }, + ["BookingNewInputSchema"]: { + durationType:PartialObjects["DurationTypeEnum"], + startToEnd?:PartialObjects["DateRangeComponentInput"], + slot?:PartialObjects["SlotComponentInput"], + capacity?:number, + comments?:string, + ressourceModel:PartialObjects["BookingsRessourceEnum"], + ressourceId:string, + checkoutInfo?:PartialObjects["CheckoutEngineInputI"] +}, + ["BookingSchema"]: { + __typename?: "BookingSchema"; + durationType?:PartialObjects["DurationTypeEnum"], + startToEnd?:PartialObjects["DateRangeComponent"], + slot?:PartialObjects["SlotComponent"], + capacity?:number, + comments?:string, + status?:PartialObjects["StatusEnum"], + paths?:PartialObjects["EnginePathComponent"][], + ownerId?:string, + dates?:PartialObjects["DateRangeComponent"][], + _id?:string, + organisationId?:string, + by?:PartialObjects["MetaBy"], + permissions?:PartialObjects["MetaPermissions"], + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"] + }, + ["BookingsRessourceEnum"]:BookingsRessourceEnum, + ["BuyerInfo"]: { + firstName:string, + lastName:string, + email:string, + company?:string, + vatNumber?:string, + address:PartialObjects["AddressInput"] +}, + ["BuyerInfoComponent"]: { + __typename?: "BuyerInfoComponent"; + firstName?:string, + lastName?:string, + email?:string, + company?:string, + vatNumber?:string, + address?:PartialObjects["AddressComponent"] + }, + ["cardBrandEnum"]:cardBrandEnum, + ["CardInfo"]: { + __typename?: "CardInfo"; + brand?:PartialObjects["cardBrandEnum"], + country?:string, + exp_month?:number, + exp_year?:number, + last4?:string + }, + ["cardTypeEnum"]:cardTypeEnum, + ["CategoryModel"]: { + __typename?: "CategoryModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + title?:PartialObjects["TranslatableComponent"], + teaser?:PartialObjects["TranslatableComponent"], + cover?:PartialObjects["ImageComponent"], + thumbnail?:PartialObjects["ImageComponent"], + extraImages?:PartialObjects["ImageComponent"][], + content?:PartialObjects["TranslatableComponent"], + seo?:PartialObjects["SEOField"], + urls?:PartialObjects["TranslatableComponent"], + ressourceType?:PartialObjects["RessourceEnum"], + colorCode?:string + }, + ["CheckoutEngineInput"]: { + __typename?: "CheckoutEngineInput"; + contactInfo?:PartialObjects["BuyerInfoComponent"], + billingInfo?:PartialObjects["BuyerInfoComponent"], + token?:string + }, + ["CheckoutEngineInputI"]: { + contactInfo:PartialObjects["BuyerInfo"], + billingInfo:PartialObjects["BuyerInfo"], + token?:string +}, + ["CheckoutInput"]: { + contactInfo:PartialObjects["BuyerInfo"], + billingInfo:PartialObjects["BuyerInfo"], + token?:string +}, + ["CountryCodesComponentEnum"]:CountryCodesComponentEnum, + ["CurrencyEnum"]:CurrencyEnum, + ["DateExceptionComponent"]: { + __typename?: "DateExceptionComponent"; + dates?:PartialObjects["DateRangeComponent"], + hours?:PartialObjects["HourComponent"][], + allDay?:boolean + }, + ["DateExceptionComponentInput"]: { + dates:PartialObjects["DateRangeComponentInput"], + hours?:PartialObjects["HoursInput"][], + allDay?:boolean +}, + ["DateRangeComponent"]: { + __typename?: "DateRangeComponent"; + startDate?:PartialObjects["DateTime"], + endDate?:PartialObjects["DateTime"] + }, + ["DateRangeComponentInput"]: { + startDate:PartialObjects["DateTime"], + endDate:PartialObjects["DateTime"] +}, + ["DateTabType"]:DateTabType, + /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +["DateTime"]:any, + ["DurationTypeEnum"]:DurationTypeEnum, + ["EditAccountInput"]: { + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:PartialObjects["ImageInput"], + phoneNumber?:string, + gender?:PartialObjects["AccountGenderEnum"], + birthDate?:PartialObjects["DateTime"], + address?:PartialObjects["AddressInput"], + language?:PartialObjects["AvailableTranslation"], + settings?:PartialObjects["SettingsInput"][] +}, + ["EditMessageInput"]: { + message:string +}, + ["EditWorkspaceInput"]: { + title?:string, + teaser?:string, + cover?:PartialObjects["ImageInput"], + thumbnail?:PartialObjects["ImageInput"], + extraImages?:PartialObjects["ImageInput"][], + content?:string, + seo?:PartialObjects["SEOSimpleInput"], + urls?:string, + workspaceType?:PartialObjects["WorkspaceTypeEnum"], + pricingPerHour?:number, + currency:PartialObjects["AvailableCurrency"], + maxCapacity?:number, + minStay?:number, + maxStay?:number, + availability?:PartialObjects["AvailabilityInput"], + mainImages:PartialObjects["MainImage"], + equipmentIds?:string[], + featureIds?:string[], + address?:PartialObjects["AddressStrictInput"] +}, + ["EnginePathComponent"]: { + __typename?: "EnginePathComponent"; + ressourceModel?:PartialObjects["ModelLoadersEnum"], + ressourceId?:string + }, + ["FavoriteComponent"]: { + __typename?: "FavoriteComponent"; + ressourceId?:string, + ressourceType?:PartialObjects["RessourceEnum"], + addedAt?:PartialObjects["DateTime"], + favorite?:boolean, + liked?:boolean + }, + ["FirebaseTokenResult"]: { + __typename?: "FirebaseTokenResult"; + localId?:string, + email?:string, + displayName?:string, + idToken?:string, + registered?:boolean, + refreshToken?:string, + expiresIn?:string + }, + ["GeolocAddressSearchInput"]: { + formattedAddress:string, + radius?:number +}, + ["GeolocDistComponent"]: { + __typename?: "GeolocDistComponent"; + calculated?:number, + location?:PartialObjects["LocComponent"] + }, + ["GeolocSearchInput"]: { + longitude:number, + latitude:number, + radius?:number +}, + ["GetArgs"]: { + limit:number, + skip:number, + sort?:string +}, + ["HourComponent"]: { + __typename?: "HourComponent"; + from?:string, + to?:string + }, + ["HoursInput"]: { + from:string, + to:string +}, + ["IEngineSchema"]:{ + _id?:string; + organisationId?:string; + paths?:PartialObjects["EnginePathComponent"][]; + by?:PartialObjects["MetaBy"]; + permissions?:PartialObjects["MetaPermissions"]; + createdAt?:PartialObjects["DateTime"]; + updatedAt?:PartialObjects["DateTime"] +} & (PartialObjects["BookingEngineSchema"] | PartialObjects["BookingSchema"] | PartialObjects["OrderEngineSchema"]), + ["ImageComponent"]: { + __typename?: "ImageComponent"; + title?:string, + fileType?:string, + large?:string, + medium?:string, + small?:string + }, + ["ImageInput"]: { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +}, + ["InvoiceInfo"]: { + __typename?: "InvoiceInfo"; + provider?:PartialObjects["InvoicingProvider"], + invoiceId?:string, + invoiceNumber?:number + }, + ["InvoicingProvider"]:InvoicingProvider, + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +["JSONObject"]:any, + ["LineItemSchema"]: { + __typename?: "LineItemSchema"; + _id?:string, + productRessource?:PartialObjects["ProductRessourceEnum"], + productId?:string, + title?:string, + price?:number, + salesPrice?:number, + quantity?:number, + vatClassId?:string, + parentId?:string, + addOns?:PartialObjects["AddOnComponent"][], + localeInfo?:PartialObjects["LocaleInfo"], + promoId?:string, + finalPrice?:number, + getProduct?:PartialObjects["ProductUnion"], + getLinePrice?:number, + getLineVatPrice?:number + }, + ["LinkEmailInput"]: { + email:string, + password:string, + idToken:string +}, + ["ListEnum"]:ListEnum, + ["ListModel"]: { + __typename?: "ListModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + title?:PartialObjects["TranslatableComponent"], + teaser?:PartialObjects["TranslatableComponent"], + cover?:PartialObjects["ImageComponent"], + thumbnail?:PartialObjects["ImageComponent"], + extraImages?:PartialObjects["ImageComponent"][], + content?:PartialObjects["TranslatableComponent"], + seo?:PartialObjects["SEOField"], + urls?:PartialObjects["TranslatableComponent"], + ressourceType?:PartialObjects["ListEnum"], + colorCode?:string + }, + ["LocaleInfo"]: { + __typename?: "LocaleInfo"; + countryCode?:string, + locale?:PartialObjects["AvailableTranslation"] + }, + ["LocComponent"]: { + __typename?: "LocComponent"; + type?:string, + coordinates?:number[] + }, + ["LoginInput"]: { + email:string, + password:string +}, + ["MainImage"]: { + main?:PartialObjects["ImageInput"], + one?:PartialObjects["ImageInput"], + two?:PartialObjects["ImageInput"], + three?:PartialObjects["ImageInput"], + four?:PartialObjects["ImageInput"] +}, + ["MainImageComponent"]: { + __typename?: "MainImageComponent"; + main?:PartialObjects["ImageComponent"], + one?:PartialObjects["ImageComponent"], + two?:PartialObjects["ImageComponent"], + three?:PartialObjects["ImageComponent"], + four?:PartialObjects["ImageComponent"] + }, + ["MessageModel"]: { + __typename?: "MessageModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + roomId?:string, + notified?:boolean, + message?:string, + edited?:boolean, + deleted?:boolean, + file?:PartialObjects["ImageComponent"], + sentBy?:string, + readBy?:PartialObjects["MessageReadBy"][] + }, + ["MessageReadBy"]: { + __typename?: "MessageReadBy"; + accountId?:string, + readAt?:PartialObjects["DateTime"] + }, + ["MetaBy"]: { + __typename?: "MetaBy"; + createdBy?:string, + updatedBy?:string, + deletedBy?:string + }, + ["MetaPermissions"]: { + __typename?: "MetaPermissions"; + r?:string[], + w?:string[], + d?:string[] + }, + ["ModelLoadersEnum"]:ModelLoadersEnum, + ["Mutation"]: { + __typename?: "Mutation"; + updateMe?:PartialObjects["AccountModel"], + updateMeEmail?:PartialObjects["AccountModel"], + updateMePassword?:PartialObjects["AccountModel"], + resetPassword?:PartialObjects["SimpleResult"], + registerGuest?:PartialObjects["AccountModel"], + register?:PartialObjects["AccountModel"], + accountsBillingInfosAddOne?:PartialObjects["BillingAddressComponent"], + accountsBillingInfosEditOne?:PartialObjects["BillingAddressComponent"], + accountsBillingInfosDeleteOne?:string, + accountsPaymentMethodsAddOne?:PartialObjects["PaymentMethodDetail"], + accountsPaymentMethodsMarkasDefault?:PartialObjects["PaymentMethodDetail"], + accountsPaymentMethodsDeleteOne?:string, + accountsLinkWithStripe?:string, + accountsRefreshInfoStripe?:PartialObjects["AccountModel"], + accountsFavoritesAddOne?:PartialObjects["FavoriteComponent"], + accountsFavoritesRemoveOne?:PartialObjects["SuccessResponse"], + workspacesAddOne?:PartialObjects["WorkspaceModel"], + workspacesEditOne?:PartialObjects["WorkspaceModel"], + workspacesDeleteOne?:PartialObjects["WorkspaceModel"], + finaliseCheckoutWithStripe?:PartialObjects["OrderEngineSchema"], + removeCurrentOrder?:PartialObjects["OrderEngineSchema"], + validateOrder?:PartialObjects["OrderEngineSchema"], + roomsCreateOne?:PartialObjects["RoomModel"], + roomsAddPeople?:PartialObjects["RoomModel"], + roomsRemovePeople?:PartialObjects["RoomModel"], + roomsMarkAllMessageAsRead?:PartialObjects["RoomModel"], + reserveOneBooking?:PartialObjects["OrderEngineSchema"], + roomsAddOneMessage?:PartialObjects["MessageModel"], + roomsEditOneMessage?:PartialObjects["MessageModel"], + roomsMarkMessageAsRead?:PartialObjects["MessageModel"], + roomsDeleteOneMessage?:PartialObjects["MessageModel"] + }, + ["NewAccountInput"]: { + email:string, + password:string, + passwordConfirmation:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:PartialObjects["ImageInput"], + phoneNumber?:string, + gender?:PartialObjects["AccountGenderEnum"], + birthDate?:PartialObjects["DateTime"], + address?:PartialObjects["AddressInput"], + language?:PartialObjects["AvailableTranslation"], + settings?:PartialObjects["SettingsInput"][], + terms:boolean +}, + ["NewEmailInput"]: { + newEmail:string +}, + ["NewMessageInput"]: { + roomId:string, + message:string, + file?:PartialObjects["ImageInput"] +}, + ["NewPasswordInput"]: { + oldPassword:string, + newPassword:string, + newPasswordConfirmation:string +}, + ["NewRoomInput"]: { + accountIds:string[] +}, + ["NewWorkspaceInput"]: { + title?:string, + teaser?:string, + cover?:PartialObjects["ImageInput"], + thumbnail?:PartialObjects["ImageInput"], + extraImages?:PartialObjects["ImageInput"][], + content?:string, + seo?:PartialObjects["SEOSimpleInput"], + urls?:string, + workspaceType:PartialObjects["WorkspaceTypeEnum"], + pricingPerHour:number, + currency:PartialObjects["AvailableCurrency"], + maxCapacity:number, + minStay:number, + maxStay?:number, + availability:PartialObjects["AvailabilityInput"], + mainImages:PartialObjects["MainImage"], + equipmentIds:string[], + featureIds:string[], + address:PartialObjects["AddressStrictInput"] +}, + ["OrderBusinessStatusEnum"]:OrderBusinessStatusEnum, + ["OrderEngineSchema"]: { + __typename?: "OrderEngineSchema"; + lines?:PartialObjects["LineItemSchema"][], + localeInfo?:PartialObjects["LocaleInfo"], + currency?:PartialObjects["CurrencyEnum"], + vatExempt?:boolean, + accountId?:string, + contactInfo?:PartialObjects["BuyerInfoComponent"], + billingInfo?:PartialObjects["BuyerInfoComponent"], + promoId?:string, + invoiceInfo?:PartialObjects["InvoiceInfo"], + providerOrderItems?:PartialObjects["ProviderSchema"][], + orderName?:PartialObjects["TranslatableComponent"], + orderStatus?:PartialObjects["OrderStatusEnum"], + paymentIntent?:PartialObjects["PaymentIntentInfo"], + paymentStatus?:PartialObjects["PaymentStatusEnum"], + paymentInfo?:PartialObjects["PaymentInfo"], + subTotalPrice?:number, + vatPrice?:number, + finalPrice?:number, + promo?:PartialObjects["PromoModel"], + _id?:string, + organisationId?:string, + paths?:PartialObjects["EnginePathComponent"][], + by?:PartialObjects["MetaBy"], + permissions?:PartialObjects["MetaPermissions"], + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + getBookingInput?:PartialObjects["BookingNewInput"] + }, + ["OrderStatusEnum"]:OrderStatusEnum, + ["PaymentInfo"]: { + __typename?: "PaymentInfo"; + provider?:PartialObjects["PaymentProvider"], + transactionId?:string + }, + ["PaymentIntentInfo"]: { + __typename?: "PaymentIntentInfo"; + provider?:PartialObjects["PaymentProvider"], + stripePaymentIntentData?:PartialObjects["StripePaymentIntentData"] + }, + ["PaymentMethodDetail"]: { + __typename?: "PaymentMethodDetail"; + _id?:string, + sPayMethodId?:string, + default?:boolean, + nameOnCard?:string, + type?:PartialObjects["cardTypeEnum"], + cardInfo?:PartialObjects["CardInfo"] + }, + ["PaymentMethodInput"]: { + sPayMethodId:string, + nameOnCard:string, + default?:boolean +}, + ["PaymentProvider"]:PaymentProvider, + ["PaymentStatusEnum"]:PaymentStatusEnum, + ["PlaceComponent"]: { + __typename?: "PlaceComponent"; + placeId?:string, + address?:PartialObjects["AddressComponent"], + loc?:PartialObjects["LocComponent"], + formattedAddress?:string + }, + ["ProductRessourceEnum"]:ProductRessourceEnum, + ["ProductUnion"]: PartialObjects["WorkspaceModel"], + ["PromoModel"]: { + __typename?: "PromoModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + code?:string, + description?:PartialObjects["TranslatableComponent"], + type?:PartialObjects["PromoType"], + value?:number, + validity?:PartialObjects["DateTime"], + cummulable?:boolean, + usageLimit?:number, + usage?:number + }, + ["PromoType"]:PromoType, + ["ProviderSchema"]: { + __typename?: "ProviderSchema"; + _id?:string, + organisationId?:string, + orderStatus?:PartialObjects["OrderBusinessStatusEnum"], + lines?:PartialObjects["LineItemSchema"][] + }, + ["Query"]: { + __typename?: "Query"; + me?:PartialObjects["AccountModel"], + login?:PartialObjects["FirebaseTokenResult"], + magicLink?:PartialObjects["SimpleResult"], + placesAutocomplete?:PartialObjects["JSONObject"][], + placesGeocode?:PartialObjects["JSONObject"], + placesGeocodeFromAddress?:PartialObjects["JSONObject"], + categoriesGetOne?:PartialObjects["CategoryModel"], + categoriesGetMany?:PartialObjects["CategoryModel"][], + categoriesGetCount?:number, + listsGetOne?:PartialObjects["ListModel"], + listsGetMany?:PartialObjects["ListModel"][], + listsGetCount?:number, + accountsDashboardLinkStripe?:string, + workspacesGetOne?:PartialObjects["WorkspaceModel"], + workspacesGetMany?:PartialObjects["WorkspaceModel"][], + workspacesGetCount?:number, + workspacesSearchMany?:PartialObjects["WorkspaceModel"][], + workspacesGetMine?:PartialObjects["WorkspaceModel"][], + workspacesGetMineCount?:number, + bookingsGetOne?:PartialObjects["BookingEngineSchema"], + bookingsGetMany?:PartialObjects["BookingEngineSchema"][], + bookingsGetCount?:number, + myCurrentOrder?:PartialObjects["OrderEngineSchema"], + myOrdersGetOne?:PartialObjects["OrderEngineSchema"], + myOrdersGetMany?:PartialObjects["OrderEngineSchema"][], + myOrdersGetManyCount?:number, + roomsGetOne?:PartialObjects["RoomModel"], + roomsGetMany?:PartialObjects["RoomModel"][], + roomsGetMessages?:PartialObjects["MessageModel"][] + }, + ["RangeType"]:RangeType, + ["RemoveInput"]: { + ressourceId:string +}, + ["RemovePeopleFromRoomInput"]: { + roomId:string, + accountIds:string[] +}, + ["ResetPasswordInput"]: { + email:string +}, + ["RessourceEnum"]:RessourceEnum, + ["RoomModel"]: { + __typename?: "RoomModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + title?:string, + stats?:PartialObjects["RoomsStats"], + getAccounts?:PartialObjects["AccountModel"][], + getMessages?:PartialObjects["MessageModel"][], + lastMessage?:PartialObjects["DateTime"] + }, + ["RoomsStats"]: { + __typename?: "RoomsStats"; + unreadCounts?:PartialObjects["UnreadCount"][] + }, + ["SEOField"]: { + __typename?: "SEOField"; + title?:PartialObjects["TranslatableComponent"], + description?:PartialObjects["TranslatableComponent"], + keywords?:PartialObjects["TranslatableComponent"], + thumbnail?:PartialObjects["ImageComponent"] + }, + ["SEOSimpleField"]: { + __typename?: "SEOSimpleField"; + title?:string, + description?:string, + keywords?:string, + thumbnail?:PartialObjects["ImageComponent"] + }, + ["SEOSimpleInput"]: { + title:string, + description:string, + keywords?:string, + thumbnail?:PartialObjects["ImageInput"] +}, + ["SettingsComponent"]: { + __typename?: "SettingsComponent"; + type?:PartialObjects["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean + }, + ["SettingsInput"]: { + type:PartialObjects["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +}, + ["SettingsTypeEnum"]:SettingsTypeEnum, + ["SimpleResult"]: { + __typename?: "SimpleResult"; + message?:string + }, + ["SlotComponent"]: { + __typename?: "SlotComponent"; + startDate?:PartialObjects["DateTime"], + endDate?:PartialObjects["DateTime"], + startTime?:string, + endTime?:string + }, + ["SlotComponentInput"]: { + startDate:PartialObjects["DateTime"], + endDate:PartialObjects["DateTime"], + startTime:string, + endTime:string +}, + ["StatusEnum"]:StatusEnum, + ["StripeInfo"]: { + __typename?: "StripeInfo"; + customerId?:string + }, + ["StripePaymentIntentData"]: { + __typename?: "StripePaymentIntentData"; + id?:string, + client_secret?:string, + currency?:string, + customer?:string, + status?:string + }, + ["StripePayoutInfo"]: { + __typename?: "StripePayoutInfo"; + accountId?:string, + chargesEnabled?:boolean, + payoutsEnabled?:boolean, + detailsSubmitted?:boolean + }, + ["SuccessResponse"]: { + __typename?: "SuccessResponse"; + success?:boolean + }, + ["TranslatableComponent"]: { + __typename?: "TranslatableComponent"; + en?:string, + fr?:string, + nl?:string, + de?:string + }, + ["UnreadCount"]: { + __typename?: "UnreadCount"; + accountId?:string, + count?:number + }, + ["WorkspaceModel"]: { + __typename?: "WorkspaceModel"; + _id?:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt?:PartialObjects["DateTime"], + updatedAt?:PartialObjects["DateTime"], + r?:string[], + w?:string[], + d?:string[], + title?:string, + teaser?:string, + cover?:PartialObjects["ImageComponent"], + thumbnail?:PartialObjects["ImageComponent"], + extraImages?:PartialObjects["ImageComponent"][], + content?:string, + seo?:PartialObjects["SEOSimpleField"], + urls?:string, + place?:PartialObjects["PlaceComponent"], + dist?:PartialObjects["GeolocDistComponent"], + getDistFromLocation?:number, + workspaceType?:PartialObjects["WorkspaceTypeEnum"], + pricingPerHour?:number, + currency?:PartialObjects["AvailableCurrency"], + maxCapacity?:number, + minStay?:number, + maxStay?:number, + availability?:PartialObjects["AvailabilityComponent"], + mainImages?:PartialObjects["MainImageComponent"], + equipmentIds?:string[], + featureIds?:string[], + address?:PartialObjects["AddressComponent"], + finalPrice?:number, + getEquipments?:(PartialObjects["ListModel"] | undefined)[], + getFeatures?:(PartialObjects["ListModel"] | undefined)[], + getBookings?:(PartialObjects["BookingSchema"] | undefined)[], + getOwner?:PartialObjects["WorkspaceOwnerComponent"] + }, + ["WorkspaceOwnerComponent"]: { + __typename?: "WorkspaceOwnerComponent"; + firstName?:string, + lastName?:string + }, + ["WorkspaceTypeEnum"]:WorkspaceTypeEnum + } + +export enum AccountGenderEnum { + m = "m", + f = "f", + other = "other" +} + +export type AccountModel = { + __typename?: "AccountModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + types:AccountTypeEnum[], + organisationIds?:string[], + email:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ImageComponent, + phoneNumber?:string, + gender?:AccountGenderEnum, + birthDate?:DateTime, + getAge?:number, + address?:AddressComponent, + language?:AvailableTranslation, + settings?:SettingsComponent[], + paymentInfo?:AccountPaymentInfo, + payoutInfo?:AccountPayoutInfo, + terms:boolean, + favLikes?:(FavoriteComponent | undefined)[], + getFavorites:FavoriteComponent[], + getLiked:FavoriteComponent[] +} + +export type AccountPaymentInfo = { + __typename?: "AccountPaymentInfo", + stripeInfo?:StripeInfo, + paymentMethods?:PaymentMethodDetail[], + billingInfos?:BillingAddressComponent[] +} + +export type AccountPayoutInfo = { + __typename?: "AccountPayoutInfo", + stripeInfo?:StripePayoutInfo +} + +export enum AccountTypeEnum { + admin = "admin", + user = "user", + pro = "pro", + public = "public", + organisationOwner = "organisationOwner", + storeManager = "storeManager" +} + +export type AddInput = { + ressourceId:string, + ressourceType:RessourceEnum +} + +export type AddOnComponent = { + __typename?: "AddOnComponent", + addOnId:string, + quantity:number +} + +export type AddPeopleToRoomInput = { + roomId:string, + accountIds:string[] +} + +export type AddressComponent = { + __typename?: "AddressComponent", + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +} + +export type AddressInput = { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +} + +export type AddressStrictComponent = { + __typename?: "AddressStrictComponent", + number:string, + street:string, + zip:string, + city:string, + country:string, + streetBis?:string, + floor?:string, + box?:string, + state?:string +} + +export type AddressStrictInput = { + number:string, + street:string, + zip:string, + city:string, + country:string, + streetBis?:string, + floor?:string, + box?:string, + state?:string +} + +export type AvailabilityComponent = { + __typename?: "AvailabilityComponent", + dates:DateRangeComponent, + hours?:HourComponent[], + exceptions?:DateExceptionComponent[], + noWeekend?:boolean +} + +export type AvailabilityInput = { + dates:DateRangeComponentInput, + hours?:HoursInput[], + exceptions?:DateExceptionComponentInput[], + noWeekend?:boolean +} + +export enum AvailableCurrency { + eur = "eur", + usd = "usd" +} + +export enum AvailableTranslation { + en = "en", + fr = "fr", + nl = "nl", + de = "de" +} + +export type BillingAddressComponent = { + __typename?: "BillingAddressComponent", + firstName:string, + lastName:string, + address:AddressStrictComponent, + email?:string, + default?:boolean, + customTags?:JSONObject, + company?:string, + vatNumber?:string, + fiscalForm?:string, + _id:string +} + +export type BillingAddressInput = { + firstName:string, + lastName:string, + address:AddressStrictInput, + email?:string, + default?:boolean, + customTags?:JSONObject, + company?:string, + vatNumber?:string, + fiscalForm?:string +} + +export type BookingEngineSchema = { + __typename?: "BookingEngineSchema", + durationType:DurationTypeEnum, + startToEnd?:DateRangeComponent, + slot?:SlotComponent, + capacity?:number, + comments?:string, + status:StatusEnum, + paths?:EnginePathComponent[], + ownerId:string, + dates:DateRangeComponent[], + owner:AccountModel, + _id:string, + organisationId?:string, + by?:MetaBy, + permissions?:MetaPermissions, + createdAt:DateTime, + updatedAt:DateTime, + workspace?:WorkspaceModel, + room?:RoomModel +} + +export type BookingNewInput = { + __typename?: "BookingNewInput", + durationType:DurationTypeEnum, + startToEnd?:DateRangeComponent, + slot?:SlotComponent, + capacity?:number, + comments?:string, + ressourceModel:BookingsRessourceEnum, + ressourceId:string, + checkoutInfo?:CheckoutEngineInput +} + +export type BookingNewInputSchema = { + durationType:DurationTypeEnum, + startToEnd?:DateRangeComponentInput, + slot?:SlotComponentInput, + capacity?:number, + comments?:string, + ressourceModel:BookingsRessourceEnum, + ressourceId:string, + checkoutInfo?:CheckoutEngineInputI +} + +export type BookingSchema = { + __typename?: "BookingSchema", + durationType:DurationTypeEnum, + startToEnd?:DateRangeComponent, + slot?:SlotComponent, + capacity:number, + comments?:string, + status:StatusEnum, + paths?:EnginePathComponent[], + ownerId:string, + dates:DateRangeComponent[], + _id:string, + organisationId?:string, + by?:MetaBy, + permissions?:MetaPermissions, + createdAt:DateTime, + updatedAt:DateTime +} + +export enum BookingsRessourceEnum { + workspaces = "workspaces" +} + +export type BuyerInfo = { + firstName:string, + lastName:string, + email:string, + company?:string, + vatNumber?:string, + address:AddressInput +} + +export type BuyerInfoComponent = { + __typename?: "BuyerInfoComponent", + firstName:string, + lastName:string, + email:string, + company?:string, + vatNumber?:string, + address:AddressComponent +} + +export enum cardBrandEnum { + amex = "amex", + diners = "diners", + discover = "discover", + jcb = "jcb", + mastercard = "mastercard", + unionpay = "unionpay", + visa = "visa", + unknown = "unknown" +} + +export type CardInfo = { + __typename?: "CardInfo", + brand:cardBrandEnum, + country:string, + exp_month:number, + exp_year:number, + last4:string +} + +export enum cardTypeEnum { + alipay = "alipay", + au_becs_debit = "au_becs_debit", + bacs_debit = "bacs_debit", + bancontact = "bancontact", + card = "card", + eps = "eps", + fpx = "fpx", + giropay = "giropay", + ideal = "ideal", + p24 = "p24", + sepa_debit = "sepa_debit", + sofort = "sofort" +} + +export type CategoryModel = { + __typename?: "CategoryModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + title:TranslatableComponent, + teaser?:TranslatableComponent, + cover?:ImageComponent, + thumbnail?:ImageComponent, + extraImages?:ImageComponent[], + content?:TranslatableComponent, + seo?:SEOField, + urls?:TranslatableComponent, + ressourceType:RessourceEnum, + colorCode?:string +} + +export type CheckoutEngineInput = { + __typename?: "CheckoutEngineInput", + contactInfo:BuyerInfoComponent, + billingInfo:BuyerInfoComponent, + token?:string +} + +export type CheckoutEngineInputI = { + contactInfo:BuyerInfo, + billingInfo:BuyerInfo, + token?:string +} + +export type CheckoutInput = { + contactInfo:BuyerInfo, + billingInfo:BuyerInfo, + token?:string +} + +export enum CountryCodesComponentEnum { + AF = "AF", + AX = "AX", + AL = "AL", + DZ = "DZ", + AS = "AS", + AD = "AD", + AO = "AO", + AI = "AI", + AQ = "AQ", + AG = "AG", + AR = "AR", + AM = "AM", + AW = "AW", + AU = "AU", + AT = "AT", + AZ = "AZ", + BS = "BS", + BH = "BH", + BD = "BD", + BB = "BB", + BY = "BY", + BE = "BE", + BZ = "BZ", + BJ = "BJ", + BM = "BM", + BT = "BT", + BO = "BO", + BA = "BA", + BW = "BW", + BV = "BV", + BR = "BR", + IO = "IO", + BN = "BN", + BG = "BG", + BF = "BF", + BI = "BI", + KH = "KH", + CM = "CM", + CA = "CA", + CV = "CV", + KY = "KY", + CF = "CF", + TD = "TD", + CL = "CL", + CN = "CN", + CX = "CX", + CC = "CC", + CO = "CO", + KM = "KM", + CG = "CG", + CD = "CD", + CK = "CK", + CR = "CR", + CI = "CI", + HR = "HR", + CU = "CU", + CY = "CY", + CZ = "CZ", + DK = "DK", + DJ = "DJ", + DM = "DM", + DO = "DO", + EC = "EC", + EG = "EG", + SV = "SV", + GQ = "GQ", + ER = "ER", + EE = "EE", + ET = "ET", + FK = "FK", + FO = "FO", + FJ = "FJ", + FI = "FI", + FR = "FR", + GF = "GF", + PF = "PF", + TF = "TF", + GA = "GA", + GM = "GM", + GE = "GE", + DE = "DE", + GH = "GH", + GI = "GI", + GR = "GR", + GL = "GL", + GD = "GD", + GP = "GP", + GU = "GU", + GT = "GT", + GG = "GG", + GN = "GN", + GW = "GW", + GY = "GY", + HT = "HT", + HM = "HM", + VA = "VA", + HN = "HN", + HK = "HK", + HU = "HU", + IS = "IS", + IN = "IN", + ID = "ID", + IR = "IR", + IQ = "IQ", + IE = "IE", + IM = "IM", + IL = "IL", + IT = "IT", + JM = "JM", + JP = "JP", + JE = "JE", + JO = "JO", + KZ = "KZ", + KE = "KE", + KI = "KI", + KR = "KR", + KW = "KW", + KG = "KG", + LA = "LA", + LV = "LV", + LB = "LB", + LS = "LS", + LR = "LR", + LY = "LY", + LI = "LI", + LT = "LT", + LU = "LU", + MO = "MO", + MK = "MK", + MG = "MG", + MW = "MW", + MY = "MY", + MV = "MV", + ML = "ML", + MT = "MT", + MH = "MH", + MQ = "MQ", + MR = "MR", + MU = "MU", + YT = "YT", + MX = "MX", + FM = "FM", + MD = "MD", + MC = "MC", + MN = "MN", + ME = "ME", + MS = "MS", + MA = "MA", + MZ = "MZ", + MM = "MM", + NA = "NA", + NR = "NR", + NP = "NP", + NL = "NL", + AN = "AN", + NC = "NC", + NZ = "NZ", + NI = "NI", + NE = "NE", + NG = "NG", + NU = "NU", + NF = "NF", + MP = "MP", + NO = "NO", + OM = "OM", + PK = "PK", + PW = "PW", + PS = "PS", + PA = "PA", + PG = "PG", + PY = "PY", + PE = "PE", + PH = "PH", + PN = "PN", + PL = "PL", + PT = "PT", + PR = "PR", + QA = "QA", + RE = "RE", + RO = "RO", + RU = "RU", + RW = "RW", + BL = "BL", + SH = "SH", + KN = "KN", + LC = "LC", + MF = "MF", + PM = "PM", + VC = "VC", + WS = "WS", + SM = "SM", + ST = "ST", + SA = "SA", + SN = "SN", + RS = "RS", + SC = "SC", + SL = "SL", + SG = "SG", + SK = "SK", + SI = "SI", + SB = "SB", + SO = "SO", + ZA = "ZA", + GS = "GS", + ES = "ES", + LK = "LK", + SD = "SD", + SR = "SR", + SJ = "SJ", + SZ = "SZ", + SE = "SE", + CH = "CH", + SY = "SY", + TW = "TW", + TJ = "TJ", + TZ = "TZ", + TH = "TH", + TL = "TL", + TG = "TG", + TK = "TK", + TO = "TO", + TT = "TT", + TN = "TN", + TR = "TR", + TM = "TM", + TC = "TC", + TV = "TV", + UG = "UG", + UA = "UA", + AE = "AE", + GB = "GB", + US = "US", + UM = "UM", + UY = "UY", + UZ = "UZ", + VU = "VU", + VE = "VE", + VN = "VN", + VG = "VG", + VI = "VI", + WF = "WF", + EH = "EH", + YE = "YE", + ZM = "ZM", + ZW = "ZW", + XK = "XK", + KP = "KP" +} + +export enum CurrencyEnum { + eur = "eur", + usd = "usd" +} + +export type DateExceptionComponent = { + __typename?: "DateExceptionComponent", + dates:DateRangeComponent, + hours?:HourComponent[], + allDay?:boolean +} + +export type DateExceptionComponentInput = { + dates:DateRangeComponentInput, + hours?:HoursInput[], + allDay?:boolean +} + +export type DateRangeComponent = { + __typename?: "DateRangeComponent", + startDate:DateTime, + endDate:DateTime +} + +export type DateRangeComponentInput = { + startDate:DateTime, + endDate:DateTime +} + +export enum DateTabType { + today = "today", + tomorrow = "tomorrow", + upcomming = "upcomming", + past = "past" +} + +/** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +export type DateTime = any + +export enum DurationTypeEnum { + startToEnd = "startToEnd", + slot = "slot" +} + +export type EditAccountInput = { + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ImageInput, + phoneNumber?:string, + gender?:AccountGenderEnum, + birthDate?:DateTime, + address?:AddressInput, + language?:AvailableTranslation, + settings?:SettingsInput[] +} + +export type EditMessageInput = { + message:string +} + +export type EditWorkspaceInput = { + title?:string, + teaser?:string, + cover?:ImageInput, + thumbnail?:ImageInput, + extraImages?:ImageInput[], + content?:string, + seo?:SEOSimpleInput, + urls?:string, + workspaceType?:WorkspaceTypeEnum, + pricingPerHour?:number, + currency:AvailableCurrency, + maxCapacity?:number, + minStay?:number, + maxStay?:number, + availability?:AvailabilityInput, + mainImages:MainImage, + equipmentIds?:string[], + featureIds?:string[], + address?:AddressStrictInput +} + +export type EnginePathComponent = { + __typename?: "EnginePathComponent", + ressourceModel:ModelLoadersEnum, + ressourceId:string +} + +export type FavoriteComponent = { + __typename?: "FavoriteComponent", + ressourceId:string, + ressourceType:RessourceEnum, + addedAt:DateTime, + favorite?:boolean, + liked?:boolean +} + +export type FirebaseTokenResult = { + __typename?: "FirebaseTokenResult", + localId:string, + email?:string, + displayName?:string, + idToken:string, + registered?:boolean, + refreshToken:string, + expiresIn:string +} + +export type GeolocAddressSearchInput = { + formattedAddress:string, + radius?:number +} + +export type GeolocDistComponent = { + __typename?: "GeolocDistComponent", + calculated?:number, + location?:LocComponent +} + +export type GeolocSearchInput = { + longitude:number, + latitude:number, + radius?:number +} + +export type GetArgs = { + limit:number, + skip:number, + sort?:string +} + +export type HourComponent = { + __typename?: "HourComponent", + from:string, + to:string +} + +export type HoursInput = { + from:string, + to:string +} + +export type IEngineSchema = { + __interface:{ + _id:string, + organisationId?:string, + paths?:EnginePathComponent[], + by?:MetaBy, + permissions?:MetaPermissions, + createdAt:DateTime, + updatedAt:DateTime + }; + __resolve:{ + ['...on BookingEngineSchema']: BookingEngineSchema; + ['...on BookingSchema']: BookingSchema; + ['...on OrderEngineSchema']: OrderEngineSchema; + } +} + +export type ImageComponent = { + __typename?: "ImageComponent", + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +} + +export type ImageInput = { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +} + +export type InvoiceInfo = { + __typename?: "InvoiceInfo", + provider:InvoicingProvider, + invoiceId:string, + invoiceNumber:number +} + +export enum InvoicingProvider { + directInvoice = "directInvoice" +} + +/** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +export type JSONObject = any + +export type LineItemSchema = { + __typename?: "LineItemSchema", + _id:string, + productRessource:ProductRessourceEnum, + productId:string, + title:string, + price:number, + salesPrice:number, + quantity:number, + vatClassId:string, + parentId?:string, + addOns?:AddOnComponent[], + localeInfo?:LocaleInfo, + promoId?:string, + finalPrice?:number, + getProduct?:ProductUnion, + getLinePrice:number, + getLineVatPrice:number +} + +export type LinkEmailInput = { + email:string, + password:string, + idToken:string +} + +export enum ListEnum { + equipment = "equipment", + feature = "feature" +} + +export type ListModel = { + __typename?: "ListModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + title:TranslatableComponent, + teaser?:TranslatableComponent, + cover?:ImageComponent, + thumbnail?:ImageComponent, + extraImages?:ImageComponent[], + content?:TranslatableComponent, + seo?:SEOField, + urls?:TranslatableComponent, + ressourceType:ListEnum, + colorCode?:string +} + +export type LocaleInfo = { + __typename?: "LocaleInfo", + countryCode?:string, + locale?:AvailableTranslation +} + +export type LocComponent = { + __typename?: "LocComponent", + type:string, + coordinates:number[] +} + +export type LoginInput = { + email:string, + password:string +} + +export type MainImage = { + main?:ImageInput, + one?:ImageInput, + two?:ImageInput, + three?:ImageInput, + four?:ImageInput +} + +export type MainImageComponent = { + __typename?: "MainImageComponent", + main?:ImageComponent, + one?:ImageComponent, + two?:ImageComponent, + three?:ImageComponent, + four?:ImageComponent +} + +export type MessageModel = { + __typename?: "MessageModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + roomId:string, + notified?:boolean, + message:string, + edited:boolean, + deleted:boolean, + file?:ImageComponent, + sentBy:string, + readBy:MessageReadBy[] +} + +export type MessageReadBy = { + __typename?: "MessageReadBy", + accountId:string, + readAt:DateTime +} + +export type MetaBy = { + __typename?: "MetaBy", + createdBy?:string, + updatedBy?:string, + deletedBy?:string +} + +export type MetaPermissions = { + __typename?: "MetaPermissions", + r:string[], + w:string[], + d:string[] +} + +export enum ModelLoadersEnum { + streams = "streams", + accounts = "accounts", + workspaces = "workspaces", + bookings = "bookings" +} + +export type Mutation = { + __typename?: "Mutation", + updateMe:AccountModel, + updateMeEmail:AccountModel, + updateMePassword:AccountModel, + resetPassword:SimpleResult, + registerGuest:AccountModel, + register:AccountModel, + accountsBillingInfosAddOne:BillingAddressComponent, + accountsBillingInfosEditOne:BillingAddressComponent, + accountsBillingInfosDeleteOne:string, + accountsPaymentMethodsAddOne:PaymentMethodDetail, + accountsPaymentMethodsMarkasDefault:PaymentMethodDetail, + accountsPaymentMethodsDeleteOne:string, + accountsLinkWithStripe:string, + accountsRefreshInfoStripe:AccountModel, + accountsFavoritesAddOne:FavoriteComponent, + accountsFavoritesRemoveOne:SuccessResponse, + workspacesAddOne:WorkspaceModel, + workspacesEditOne:WorkspaceModel, + workspacesDeleteOne:WorkspaceModel, + finaliseCheckoutWithStripe:OrderEngineSchema, + removeCurrentOrder:OrderEngineSchema, + validateOrder:OrderEngineSchema, + roomsCreateOne:RoomModel, + roomsAddPeople:RoomModel, + roomsRemovePeople:RoomModel, + roomsMarkAllMessageAsRead:RoomModel, + reserveOneBooking:OrderEngineSchema, + roomsAddOneMessage:MessageModel, + roomsEditOneMessage:MessageModel, + roomsMarkMessageAsRead:MessageModel, + roomsDeleteOneMessage:MessageModel +} + +export type NewAccountInput = { + email:string, + password:string, + passwordConfirmation:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ImageInput, + phoneNumber?:string, + gender?:AccountGenderEnum, + birthDate?:DateTime, + address?:AddressInput, + language?:AvailableTranslation, + settings?:SettingsInput[], + terms:boolean +} + +export type NewEmailInput = { + newEmail:string +} + +export type NewMessageInput = { + roomId:string, + message:string, + file?:ImageInput +} + +export type NewPasswordInput = { + oldPassword:string, + newPassword:string, + newPasswordConfirmation:string +} + +export type NewRoomInput = { + accountIds:string[] +} + +export type NewWorkspaceInput = { + title?:string, + teaser?:string, + cover?:ImageInput, + thumbnail?:ImageInput, + extraImages?:ImageInput[], + content?:string, + seo?:SEOSimpleInput, + urls?:string, + workspaceType:WorkspaceTypeEnum, + pricingPerHour:number, + currency:AvailableCurrency, + maxCapacity:number, + minStay:number, + maxStay?:number, + availability:AvailabilityInput, + mainImages:MainImage, + equipmentIds:string[], + featureIds:string[], + address:AddressStrictInput +} + +export enum OrderBusinessStatusEnum { + processing = "processing", + shipped = "shipped", + completed = "completed", + canceled = "canceled", + refund = "refund" +} + +export type OrderEngineSchema = { + __typename?: "OrderEngineSchema", + lines:LineItemSchema[], + localeInfo:LocaleInfo, + currency:CurrencyEnum, + vatExempt:boolean, + accountId:string, + contactInfo?:BuyerInfoComponent, + billingInfo?:BuyerInfoComponent, + promoId?:string, + invoiceInfo?:InvoiceInfo, + providerOrderItems?:ProviderSchema[], + orderName?:TranslatableComponent, + orderStatus:OrderStatusEnum, + paymentIntent:PaymentIntentInfo, + paymentStatus?:PaymentStatusEnum, + paymentInfo?:PaymentInfo, + subTotalPrice:number, + vatPrice:number, + finalPrice:number, + promo?:PromoModel, + _id:string, + organisationId?:string, + paths?:EnginePathComponent[], + by?:MetaBy, + permissions?:MetaPermissions, + createdAt:DateTime, + updatedAt:DateTime, + getBookingInput?:BookingNewInput +} + +export enum OrderStatusEnum { + draft = "draft", + processing = "processing", + shipped = "shipped", + completed = "completed", + canceled = "canceled", + refund = "refund" +} + +export type PaymentInfo = { + __typename?: "PaymentInfo", + provider:PaymentProvider, + transactionId:string +} + +export type PaymentIntentInfo = { + __typename?: "PaymentIntentInfo", + provider:PaymentProvider, + stripePaymentIntentData:StripePaymentIntentData +} + +export type PaymentMethodDetail = { + __typename?: "PaymentMethodDetail", + _id:string, + sPayMethodId:string, + default:boolean, + nameOnCard:string, + type:cardTypeEnum, + cardInfo?:CardInfo +} + +export type PaymentMethodInput = { + sPayMethodId:string, + nameOnCard:string, + default?:boolean +} + +export enum PaymentProvider { + free = "free", + stripe = "stripe", + bancontact = "bancontact", + bankWire = "bankWire" +} + +export enum PaymentStatusEnum { + pending = "pending", + actionNeeded = "actionNeeded", + paid = "paid", + free = "free" +} + +export type PlaceComponent = { + __typename?: "PlaceComponent", + placeId:string, + address?:AddressComponent, + loc:LocComponent, + formattedAddress:string +} + +export enum ProductRessourceEnum { + workspaces = "workspaces" +} + +export type ProductUnion = { + __union:WorkspaceModel; + __resolve:{ + ['...on WorkspaceModel']: WorkspaceModel; + } +} + +export type PromoModel = { + __typename?: "PromoModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + code:string, + description?:TranslatableComponent, + type:PromoType, + value:number, + validity?:DateTime, + cummulable:boolean, + usageLimit?:number, + usage:number +} + +export enum PromoType { + fixed = "fixed", + percentage = "percentage" +} + +export type ProviderSchema = { + __typename?: "ProviderSchema", + _id:string, + organisationId:string, + orderStatus:OrderBusinessStatusEnum, + lines:LineItemSchema[] +} + +export type Query = { + __typename?: "Query", + me:AccountModel, + login:FirebaseTokenResult, + magicLink:SimpleResult, + placesAutocomplete:JSONObject[], + placesGeocode:JSONObject, + placesGeocodeFromAddress:JSONObject, + categoriesGetOne:CategoryModel, + categoriesGetMany:CategoryModel[], + categoriesGetCount:number, + listsGetOne:ListModel, + listsGetMany:ListModel[], + listsGetCount:number, + accountsDashboardLinkStripe:string, + workspacesGetOne:WorkspaceModel, + workspacesGetMany:WorkspaceModel[], + workspacesGetCount:number, + workspacesSearchMany:WorkspaceModel[], + workspacesGetMine:WorkspaceModel[], + workspacesGetMineCount:number, + bookingsGetOne:BookingEngineSchema, + bookingsGetMany:BookingEngineSchema[], + bookingsGetCount:number, + myCurrentOrder:OrderEngineSchema, + myOrdersGetOne:OrderEngineSchema, + myOrdersGetMany:OrderEngineSchema[], + myOrdersGetManyCount:number, + roomsGetOne:RoomModel, + roomsGetMany:RoomModel[], + roomsGetMessages:MessageModel[] +} + +export enum RangeType { + strict = "strict", + intersect = "intersect", + intersectLarge = "intersectLarge", + included = "included" +} + +export type RemoveInput = { + ressourceId:string +} + +export type RemovePeopleFromRoomInput = { + roomId:string, + accountIds:string[] +} + +export type ResetPasswordInput = { + email:string +} + +export enum RessourceEnum { + workspaces = "workspaces" +} + +export type RoomModel = { + __typename?: "RoomModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + title?:string, + stats:RoomsStats, + getAccounts?:AccountModel[], + getMessages?:MessageModel[], + lastMessage?:DateTime +} + +export type RoomsStats = { + __typename?: "RoomsStats", + unreadCounts:UnreadCount[] +} + +export type SEOField = { + __typename?: "SEOField", + title:TranslatableComponent, + description:TranslatableComponent, + keywords?:TranslatableComponent, + thumbnail?:ImageComponent +} + +export type SEOSimpleField = { + __typename?: "SEOSimpleField", + title:string, + description:string, + keywords?:string, + thumbnail?:ImageComponent +} + +export type SEOSimpleInput = { + title:string, + description:string, + keywords?:string, + thumbnail?:ImageInput +} + +export type SettingsComponent = { + __typename?: "SettingsComponent", + type:SettingsTypeEnum, + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +} + +export type SettingsInput = { + type:SettingsTypeEnum, + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +} + +export enum SettingsTypeEnum { + notifications = "notifications", + promotions = "promotions", + reminders = "reminders" +} + +export type SimpleResult = { + __typename?: "SimpleResult", + message:string +} + +export type SlotComponent = { + __typename?: "SlotComponent", + startDate:DateTime, + endDate:DateTime, + startTime:string, + endTime:string +} + +export type SlotComponentInput = { + startDate:DateTime, + endDate:DateTime, + startTime:string, + endTime:string +} + +export enum StatusEnum { + draft = "draft", + processing = "processing", + validated = "validated", + cancelled = "cancelled" +} + +export type StripeInfo = { + __typename?: "StripeInfo", + customerId:string +} + +export type StripePaymentIntentData = { + __typename?: "StripePaymentIntentData", + id:string, + client_secret?:string, + currency:string, + customer:string, + status:string +} + +export type StripePayoutInfo = { + __typename?: "StripePayoutInfo", + accountId:string, + chargesEnabled:boolean, + payoutsEnabled:boolean, + detailsSubmitted:boolean +} + +export type SuccessResponse = { + __typename?: "SuccessResponse", + success:boolean +} + +export type TranslatableComponent = { + __typename?: "TranslatableComponent", + en?:string, + fr:string, + nl?:string, + de?:string +} + +export type UnreadCount = { + __typename?: "UnreadCount", + accountId:string, + count:number +} + +export type WorkspaceModel = { + __typename?: "WorkspaceModel", + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:DateTime, + updatedAt:DateTime, + r:string[], + w:string[], + d:string[], + title?:string, + teaser?:string, + cover?:ImageComponent, + thumbnail?:ImageComponent, + extraImages?:ImageComponent[], + content?:string, + seo?:SEOSimpleField, + urls?:string, + place:PlaceComponent, + dist?:GeolocDistComponent, + getDistFromLocation?:number, + workspaceType:WorkspaceTypeEnum, + pricingPerHour:number, + currency:AvailableCurrency, + maxCapacity:number, + minStay:number, + maxStay?:number, + availability:AvailabilityComponent, + mainImages:MainImageComponent, + equipmentIds:string[], + featureIds:string[], + address:AddressComponent, + finalPrice:number, + getEquipments?:(ListModel | undefined)[], + getFeatures?:(ListModel | undefined)[], + getBookings?:(BookingSchema | undefined)[], + getOwner?:WorkspaceOwnerComponent +} + +export type WorkspaceOwnerComponent = { + __typename?: "WorkspaceOwnerComponent", + firstName?:string, + lastName?:string +} + +export enum WorkspaceTypeEnum { + meeting = "meeting", + individual = "individual", + open = "open" +} + +export const AllTypesProps: Record = { + AccountGenderEnum: "enum", + AccountTypeEnum: "enum", + AddInput:{ + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + } + }, + AddPeopleToRoomInput:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + accountIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + } + }, + AddressInput:{ + number:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + street:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + streetBis:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + floor:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + box:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + zip:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + state:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + city:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + country:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + AddressStrictInput:{ + number:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + street:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + zip:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + city:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + country:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + streetBis:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + floor:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + box:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + state:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + AvailabilityInput:{ + dates:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:true + }, + hours:{ + type:"HoursInput", + array:true, + arrayRequired:false, + required:true + }, + exceptions:{ + type:"DateExceptionComponentInput", + array:true, + arrayRequired:false, + required:true + }, + noWeekend:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + AvailableCurrency: "enum", + AvailableTranslation: "enum", + BillingAddressInput:{ + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + address:{ + type:"AddressStrictInput", + array:false, + arrayRequired:false, + required:true + }, + email:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + default:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + customTags:{ + type:"JSONObject", + array:false, + arrayRequired:false, + required:false + }, + company:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + vatNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + fiscalForm:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + BookingNewInputSchema:{ + durationType:{ + type:"DurationTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + startToEnd:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + slot:{ + type:"SlotComponentInput", + array:false, + arrayRequired:false, + required:false + }, + capacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + comments:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + checkoutInfo:{ + type:"CheckoutEngineInputI", + array:false, + arrayRequired:false, + required:false + } + }, + BookingsRessourceEnum: "enum", + BuyerInfo:{ + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + company:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + vatNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:true + } + }, + cardBrandEnum: "enum", + cardTypeEnum: "enum", + CheckoutEngineInputI:{ + contactInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + billingInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + token:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + CheckoutInput:{ + contactInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + billingInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + token:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + CountryCodesComponentEnum: "enum", + CurrencyEnum: "enum", + DateExceptionComponentInput:{ + dates:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:true + }, + hours:{ + type:"HoursInput", + array:true, + arrayRequired:false, + required:true + }, + allDay:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + DateRangeComponentInput:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + } + }, + DateTabType: "enum", + DateTime: "String", + DurationTypeEnum: "enum", + EditAccountInput:{ + userName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + profilePicture:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + phoneNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + gender:{ + type:"AccountGenderEnum", + array:false, + arrayRequired:false, + required:false + }, + birthDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:false + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + settings:{ + type:"SettingsInput", + array:true, + arrayRequired:false, + required:true + } + }, + EditMessageInput:{ + message:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + EditWorkspaceInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + teaser:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + cover:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + extraImages:{ + type:"ImageInput", + array:true, + arrayRequired:false, + required:true + }, + content:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + seo:{ + type:"SEOSimpleInput", + array:false, + arrayRequired:false, + required:false + }, + urls:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + pricingPerHour:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + currency:{ + type:"AvailableCurrency", + array:false, + arrayRequired:false, + required:true + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + minStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + availability:{ + type:"AvailabilityInput", + array:false, + arrayRequired:false, + required:false + }, + mainImages:{ + type:"MainImage", + array:false, + arrayRequired:false, + required:true + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + address:{ + type:"AddressStrictInput", + array:false, + arrayRequired:false, + required:false + } + }, + GeolocAddressSearchInput:{ + formattedAddress:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + radius:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + } + }, + GeolocSearchInput:{ + longitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + latitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + radius:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + } + }, + GetArgs:{ + limit:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + skip:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + sort:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + HoursInput:{ + from:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + to:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + ImageInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + fileType:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + large:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + medium:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + small:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + InvoicingProvider: "enum", + JSONObject: "String", + LinkEmailInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + idToken:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + ListEnum: "enum", + LoginInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + MainImage:{ + main:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + one:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + two:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + three:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + four:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + } + }, + ModelLoadersEnum: "enum", + Mutation:{ + updateMe:{ + input:{ + type:"EditAccountInput", + array:false, + arrayRequired:false, + required:true + } + }, + updateMeEmail:{ + input:{ + type:"NewEmailInput", + array:false, + arrayRequired:false, + required:true + } + }, + updateMePassword:{ + input:{ + type:"NewPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + resetPassword:{ + input:{ + type:"ResetPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + registerGuest:{ + otherInfo:{ + type:"EditAccountInput", + array:false, + arrayRequired:false, + required:true + }, + input:{ + type:"LinkEmailInput", + array:false, + arrayRequired:false, + required:true + } + }, + register:{ + input:{ + type:"NewAccountInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsBillingInfosAddOne:{ + input:{ + type:"BillingAddressInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsBillingInfosEditOne:{ + input:{ + type:"BillingAddressInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsBillingInfosDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsPaymentMethodsAddOne:{ + input:{ + type:"PaymentMethodInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsPaymentMethodsMarkasDefault:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsPaymentMethodsDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsFavoritesAddOne:{ + input:{ + type:"AddInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsFavoritesRemoveOne:{ + input:{ + type:"RemoveInput", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesAddOne:{ + input:{ + type:"NewWorkspaceInput", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesEditOne:{ + input:{ + type:"EditWorkspaceInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + finaliseCheckoutWithStripe:{ + input:{ + type:"CheckoutInput", + array:false, + arrayRequired:false, + required:true + } + }, + validateOrder:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsCreateOne:{ + input:{ + type:"NewRoomInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsAddPeople:{ + input:{ + type:"AddPeopleToRoomInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsRemovePeople:{ + input:{ + type:"RemovePeopleFromRoomInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsMarkAllMessageAsRead:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + reserveOneBooking:{ + input:{ + type:"BookingNewInputSchema", + array:false, + arrayRequired:false, + required:true + } + }, + roomsAddOneMessage:{ + input:{ + type:"NewMessageInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsEditOneMessage:{ + input:{ + type:"EditMessageInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsMarkMessageAsRead:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsDeleteOneMessage:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + } + }, + NewAccountInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + passwordConfirmation:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + userName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + profilePicture:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + phoneNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + gender:{ + type:"AccountGenderEnum", + array:false, + arrayRequired:false, + required:false + }, + birthDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:false + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + settings:{ + type:"SettingsInput", + array:true, + arrayRequired:false, + required:true + }, + terms:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:true + } + }, + NewEmailInput:{ + newEmail:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + NewMessageInput:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + message:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + file:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + } + }, + NewPasswordInput:{ + oldPassword:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + newPassword:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + newPasswordConfirmation:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + NewRoomInput:{ + accountIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + } + }, + NewWorkspaceInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + teaser:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + cover:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + extraImages:{ + type:"ImageInput", + array:true, + arrayRequired:false, + required:true + }, + content:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + seo:{ + type:"SEOSimpleInput", + array:false, + arrayRequired:false, + required:false + }, + urls:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + pricingPerHour:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + currency:{ + type:"AvailableCurrency", + array:false, + arrayRequired:false, + required:true + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + minStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + maxStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + availability:{ + type:"AvailabilityInput", + array:false, + arrayRequired:false, + required:true + }, + mainImages:{ + type:"MainImage", + array:false, + arrayRequired:false, + required:true + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + }, + address:{ + type:"AddressStrictInput", + array:false, + arrayRequired:false, + required:true + } + }, + OrderBusinessStatusEnum: "enum", + OrderStatusEnum: "enum", + PaymentMethodInput:{ + sPayMethodId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + nameOnCard:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + default:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + PaymentProvider: "enum", + PaymentStatusEnum: "enum", + ProductRessourceEnum: "enum", + PromoType: "enum", + Query:{ + login:{ + refreshToken:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + creds:{ + type:"LoginInput", + array:false, + arrayRequired:false, + required:false + } + }, + magicLink:{ + input:{ + type:"ResetPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + placesAutocomplete:{ + session:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + input:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + country:{ + type:"CountryCodesComponentEnum", + array:true, + arrayRequired:false, + required:true + } + }, + placesGeocode:{ + session:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + placeId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + placesGeocodeFromAddress:{ + address:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesGetMany:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + categoriesGetCount:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + listsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + listsGetMany:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + listsGetCount:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + workspacesGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesGetMany:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesGetCount:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesSearchMany:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesGetMine:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesGetMineCount:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + bookingsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + bookingsGetMany:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + rangeType:{ + type:"RangeType", + array:false, + arrayRequired:false, + required:false + }, + dateTabType:{ + type:"DateTabType", + array:false, + arrayRequired:false, + required:false + }, + dateRange:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:false + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + status:{ + type:"StatusEnum", + array:true, + arrayRequired:false, + required:true + } + }, + bookingsGetCount:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + rangeType:{ + type:"RangeType", + array:false, + arrayRequired:false, + required:false + }, + dateTabType:{ + type:"DateTabType", + array:false, + arrayRequired:false, + required:false + }, + dateRange:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:false + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + status:{ + type:"StatusEnum", + array:true, + arrayRequired:false, + required:true + } + }, + myOrdersGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + myOrdersGetMany:{ + orderStatus:{ + type:"OrderStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + paymentStatus:{ + type:"PaymentStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + myOrdersGetManyCount:{ + orderStatus:{ + type:"OrderStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + paymentStatus:{ + type:"PaymentStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + roomsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsGetMany:{ + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + roomsGetMessages:{ + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + } + }, + RangeType: "enum", + RemoveInput:{ + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + RemovePeopleFromRoomInput:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + accountIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + } + }, + ResetPasswordInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + RessourceEnum: "enum", + RoomModel:{ + getMessages:{ + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + } + }, + SEOSimpleInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + description:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + keywords:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + } + }, + SettingsInput:{ + type:{ + type:"SettingsTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + email:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + pushNotifications:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + sms:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + SettingsTypeEnum: "enum", + SlotComponentInput:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + }, + startTime:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + endTime:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + StatusEnum: "enum", + WorkspaceModel:{ + getDistFromLocation:{ + longitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + latitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + radius:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + } + }, + getBookings:{ + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:false + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + status:{ + type:"StatusEnum", + array:true, + arrayRequired:false, + required:true + }, + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + rangeType:{ + type:"RangeType", + array:false, + arrayRequired:false, + required:false + }, + dateTabType:{ + type:"DateTabType", + array:false, + arrayRequired:false, + required:false + }, + dateRange:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + } + }, + WorkspaceTypeEnum: "enum" +} + +export const ReturnTypes: Record = { + AccountModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + types:"AccountTypeEnum", + organisationIds:"String", + email:"String", + userName:"String", + firstName:"String", + lastName:"String", + profilePicture:"ImageComponent", + phoneNumber:"String", + gender:"AccountGenderEnum", + birthDate:"DateTime", + getAge:"Int", + address:"AddressComponent", + language:"AvailableTranslation", + settings:"SettingsComponent", + paymentInfo:"AccountPaymentInfo", + payoutInfo:"AccountPayoutInfo", + terms:"Boolean", + favLikes:"FavoriteComponent", + getFavorites:"FavoriteComponent", + getLiked:"FavoriteComponent" + }, + AccountPaymentInfo:{ + stripeInfo:"StripeInfo", + paymentMethods:"PaymentMethodDetail", + billingInfos:"BillingAddressComponent" + }, + AccountPayoutInfo:{ + stripeInfo:"StripePayoutInfo" + }, + AddOnComponent:{ + addOnId:"String", + quantity:"Float" + }, + AddressComponent:{ + number:"String", + street:"String", + streetBis:"String", + floor:"String", + box:"String", + zip:"String", + state:"String", + city:"String", + country:"String" + }, + AddressStrictComponent:{ + number:"String", + street:"String", + zip:"String", + city:"String", + country:"String", + streetBis:"String", + floor:"String", + box:"String", + state:"String" + }, + AvailabilityComponent:{ + dates:"DateRangeComponent", + hours:"HourComponent", + exceptions:"DateExceptionComponent", + noWeekend:"Boolean" + }, + BillingAddressComponent:{ + firstName:"String", + lastName:"String", + address:"AddressStrictComponent", + email:"String", + default:"Boolean", + customTags:"JSONObject", + company:"String", + vatNumber:"String", + fiscalForm:"String", + _id:"String" + }, + BookingEngineSchema:{ + durationType:"DurationTypeEnum", + startToEnd:"DateRangeComponent", + slot:"SlotComponent", + capacity:"Int", + comments:"String", + status:"StatusEnum", + paths:"EnginePathComponent", + ownerId:"String", + dates:"DateRangeComponent", + owner:"AccountModel", + _id:"ID", + organisationId:"ID", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime", + workspace:"WorkspaceModel", + room:"RoomModel" + }, + BookingNewInput:{ + durationType:"DurationTypeEnum", + startToEnd:"DateRangeComponent", + slot:"SlotComponent", + capacity:"Int", + comments:"String", + ressourceModel:"BookingsRessourceEnum", + ressourceId:"String", + checkoutInfo:"CheckoutEngineInput" + }, + BookingSchema:{ + durationType:"DurationTypeEnum", + startToEnd:"DateRangeComponent", + slot:"SlotComponent", + capacity:"Int", + comments:"String", + status:"StatusEnum", + paths:"EnginePathComponent", + ownerId:"String", + dates:"DateRangeComponent", + _id:"ID", + organisationId:"ID", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime" + }, + BuyerInfoComponent:{ + firstName:"String", + lastName:"String", + email:"String", + company:"String", + vatNumber:"String", + address:"AddressComponent" + }, + CardInfo:{ + brand:"cardBrandEnum", + country:"String", + exp_month:"Int", + exp_year:"Int", + last4:"String" + }, + CategoryModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"TranslatableComponent", + teaser:"TranslatableComponent", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"TranslatableComponent", + seo:"SEOField", + urls:"TranslatableComponent", + ressourceType:"RessourceEnum", + colorCode:"String" + }, + CheckoutEngineInput:{ + contactInfo:"BuyerInfoComponent", + billingInfo:"BuyerInfoComponent", + token:"String" + }, + DateExceptionComponent:{ + dates:"DateRangeComponent", + hours:"HourComponent", + allDay:"Boolean" + }, + DateRangeComponent:{ + startDate:"DateTime", + endDate:"DateTime" + }, + EnginePathComponent:{ + ressourceModel:"ModelLoadersEnum", + ressourceId:"String" + }, + FavoriteComponent:{ + ressourceId:"String", + ressourceType:"RessourceEnum", + addedAt:"DateTime", + favorite:"Boolean", + liked:"Boolean" + }, + FirebaseTokenResult:{ + localId:"String", + email:"String", + displayName:"String", + idToken:"String", + registered:"Boolean", + refreshToken:"String", + expiresIn:"String" + }, + GeolocDistComponent:{ + calculated:"Float", + location:"LocComponent" + }, + HourComponent:{ + from:"String", + to:"String" + }, + IEngineSchema:{ + "...on BookingEngineSchema": "BookingEngineSchema", + "...on BookingSchema": "BookingSchema", + "...on OrderEngineSchema": "OrderEngineSchema", + _id:"ID", + organisationId:"ID", + paths:"EnginePathComponent", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime" + }, + ImageComponent:{ + title:"String", + fileType:"String", + large:"String", + medium:"String", + small:"String" + }, + InvoiceInfo:{ + provider:"InvoicingProvider", + invoiceId:"String", + invoiceNumber:"Float" + }, + LineItemSchema:{ + _id:"String", + productRessource:"ProductRessourceEnum", + productId:"String", + title:"String", + price:"Float", + salesPrice:"Float", + quantity:"Float", + vatClassId:"String", + parentId:"String", + addOns:"AddOnComponent", + localeInfo:"LocaleInfo", + promoId:"String", + finalPrice:"Float", + getProduct:"ProductUnion", + getLinePrice:"Float", + getLineVatPrice:"Float" + }, + ListModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"TranslatableComponent", + teaser:"TranslatableComponent", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"TranslatableComponent", + seo:"SEOField", + urls:"TranslatableComponent", + ressourceType:"ListEnum", + colorCode:"String" + }, + LocaleInfo:{ + countryCode:"String", + locale:"AvailableTranslation" + }, + LocComponent:{ + type:"String", + coordinates:"Float" + }, + MainImageComponent:{ + main:"ImageComponent", + one:"ImageComponent", + two:"ImageComponent", + three:"ImageComponent", + four:"ImageComponent" + }, + MessageModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + roomId:"String", + notified:"Boolean", + message:"String", + edited:"Boolean", + deleted:"Boolean", + file:"ImageComponent", + sentBy:"String", + readBy:"MessageReadBy" + }, + MessageReadBy:{ + accountId:"String", + readAt:"DateTime" + }, + MetaBy:{ + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID" + }, + MetaPermissions:{ + r:"String", + w:"String", + d:"String" + }, + Mutation:{ + updateMe:"AccountModel", + updateMeEmail:"AccountModel", + updateMePassword:"AccountModel", + resetPassword:"SimpleResult", + registerGuest:"AccountModel", + register:"AccountModel", + accountsBillingInfosAddOne:"BillingAddressComponent", + accountsBillingInfosEditOne:"BillingAddressComponent", + accountsBillingInfosDeleteOne:"String", + accountsPaymentMethodsAddOne:"PaymentMethodDetail", + accountsPaymentMethodsMarkasDefault:"PaymentMethodDetail", + accountsPaymentMethodsDeleteOne:"String", + accountsLinkWithStripe:"String", + accountsRefreshInfoStripe:"AccountModel", + accountsFavoritesAddOne:"FavoriteComponent", + accountsFavoritesRemoveOne:"SuccessResponse", + workspacesAddOne:"WorkspaceModel", + workspacesEditOne:"WorkspaceModel", + workspacesDeleteOne:"WorkspaceModel", + finaliseCheckoutWithStripe:"OrderEngineSchema", + removeCurrentOrder:"OrderEngineSchema", + validateOrder:"OrderEngineSchema", + roomsCreateOne:"RoomModel", + roomsAddPeople:"RoomModel", + roomsRemovePeople:"RoomModel", + roomsMarkAllMessageAsRead:"RoomModel", + reserveOneBooking:"OrderEngineSchema", + roomsAddOneMessage:"MessageModel", + roomsEditOneMessage:"MessageModel", + roomsMarkMessageAsRead:"MessageModel", + roomsDeleteOneMessage:"MessageModel" + }, + OrderEngineSchema:{ + lines:"LineItemSchema", + localeInfo:"LocaleInfo", + currency:"CurrencyEnum", + vatExempt:"Boolean", + accountId:"String", + contactInfo:"BuyerInfoComponent", + billingInfo:"BuyerInfoComponent", + promoId:"ID", + invoiceInfo:"InvoiceInfo", + providerOrderItems:"ProviderSchema", + orderName:"TranslatableComponent", + orderStatus:"OrderStatusEnum", + paymentIntent:"PaymentIntentInfo", + paymentStatus:"PaymentStatusEnum", + paymentInfo:"PaymentInfo", + subTotalPrice:"Float", + vatPrice:"Float", + finalPrice:"Float", + promo:"PromoModel", + _id:"ID", + organisationId:"ID", + paths:"EnginePathComponent", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime", + getBookingInput:"BookingNewInput" + }, + PaymentInfo:{ + provider:"PaymentProvider", + transactionId:"String" + }, + PaymentIntentInfo:{ + provider:"PaymentProvider", + stripePaymentIntentData:"StripePaymentIntentData" + }, + PaymentMethodDetail:{ + _id:"String", + sPayMethodId:"String", + default:"Boolean", + nameOnCard:"String", + type:"cardTypeEnum", + cardInfo:"CardInfo" + }, + PlaceComponent:{ + placeId:"String", + address:"AddressComponent", + loc:"LocComponent", + formattedAddress:"String" + }, + ProductUnion:{ + "...on WorkspaceModel":"WorkspaceModel" + }, + PromoModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + code:"String", + description:"TranslatableComponent", + type:"PromoType", + value:"Float", + validity:"DateTime", + cummulable:"Boolean", + usageLimit:"Float", + usage:"Float" + }, + ProviderSchema:{ + _id:"String", + organisationId:"String", + orderStatus:"OrderBusinessStatusEnum", + lines:"LineItemSchema" + }, + Query:{ + me:"AccountModel", + login:"FirebaseTokenResult", + magicLink:"SimpleResult", + placesAutocomplete:"JSONObject", + placesGeocode:"JSONObject", + placesGeocodeFromAddress:"JSONObject", + categoriesGetOne:"CategoryModel", + categoriesGetMany:"CategoryModel", + categoriesGetCount:"Float", + listsGetOne:"ListModel", + listsGetMany:"ListModel", + listsGetCount:"Float", + accountsDashboardLinkStripe:"String", + workspacesGetOne:"WorkspaceModel", + workspacesGetMany:"WorkspaceModel", + workspacesGetCount:"Float", + workspacesSearchMany:"WorkspaceModel", + workspacesGetMine:"WorkspaceModel", + workspacesGetMineCount:"Float", + bookingsGetOne:"BookingEngineSchema", + bookingsGetMany:"BookingEngineSchema", + bookingsGetCount:"Float", + myCurrentOrder:"OrderEngineSchema", + myOrdersGetOne:"OrderEngineSchema", + myOrdersGetMany:"OrderEngineSchema", + myOrdersGetManyCount:"Float", + roomsGetOne:"RoomModel", + roomsGetMany:"RoomModel", + roomsGetMessages:"MessageModel" + }, + RoomModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"String", + stats:"RoomsStats", + getAccounts:"AccountModel", + getMessages:"MessageModel", + lastMessage:"DateTime" + }, + RoomsStats:{ + unreadCounts:"UnreadCount" + }, + SEOField:{ + title:"TranslatableComponent", + description:"TranslatableComponent", + keywords:"TranslatableComponent", + thumbnail:"ImageComponent" + }, + SEOSimpleField:{ + title:"String", + description:"String", + keywords:"String", + thumbnail:"ImageComponent" + }, + SettingsComponent:{ + type:"SettingsTypeEnum", + email:"Boolean", + pushNotifications:"Boolean", + sms:"Boolean" + }, + SimpleResult:{ + message:"String" + }, + SlotComponent:{ + startDate:"DateTime", + endDate:"DateTime", + startTime:"String", + endTime:"String" + }, + StripeInfo:{ + customerId:"String" + }, + StripePaymentIntentData:{ + id:"String", + client_secret:"String", + currency:"String", + customer:"String", + status:"String" + }, + StripePayoutInfo:{ + accountId:"String", + chargesEnabled:"Boolean", + payoutsEnabled:"Boolean", + detailsSubmitted:"Boolean" + }, + SuccessResponse:{ + success:"Boolean" + }, + TranslatableComponent:{ + en:"String", + fr:"String", + nl:"String", + de:"String" + }, + UnreadCount:{ + accountId:"String", + count:"Float" + }, + WorkspaceModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"String", + teaser:"String", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"String", + seo:"SEOSimpleField", + urls:"String", + place:"PlaceComponent", + dist:"GeolocDistComponent", + getDistFromLocation:"Float", + workspaceType:"WorkspaceTypeEnum", + pricingPerHour:"Int", + currency:"AvailableCurrency", + maxCapacity:"Int", + minStay:"Int", + maxStay:"Int", + availability:"AvailabilityComponent", + mainImages:"MainImageComponent", + equipmentIds:"String", + featureIds:"String", + address:"AddressComponent", + finalPrice:"Float", + getEquipments:"ListModel", + getFeatures:"ListModel", + getBookings:"BookingSchema", + getOwner:"WorkspaceOwnerComponent" + }, + WorkspaceOwnerComponent:{ + firstName:"String", + lastName:"String" + } +} + +export class GraphQLError extends Error { + constructor(public response: GraphQLResponse) { + super(""); + console.error(response); + } + toString() { + return "GraphQL Response Error"; + } + } + + + +export type UnwrapPromise = T extends Promise ? R : T; +export type ZeusState Promise> = NonNullable< + UnwrapPromise> +>; +export type ZeusHook< + T extends ( + ...args: any[] + ) => Record Promise>, + N extends keyof ReturnType +> = ZeusState[N]>; + +type Func

= (...args: P) => R; +type AnyFunc = Func; + +type WithTypeNameValue = T & { + __typename?: true; +}; + +type AliasType = WithTypeNameValue & { + __alias?: Record>; +}; + +type NotUndefined = T extends undefined ? never : T; + +export type ResolverType = NotUndefined; + +export type ArgsType = F extends Func ? P : never; + +interface GraphQLResponse { + data?: Record; + errors?: Array<{ + message: string; + }>; +} + +export type ValuesOf = T[keyof T]; + +export type MapResolve = SRC extends { + __interface: infer INTERFACE; + __resolve: Record & infer IMPLEMENTORS; + } + ? + ValuesOf<{ + [k in (keyof SRC['__resolve'] & keyof DST)]: ({ + [rk in (keyof SRC['__resolve'][k] & keyof DST[k])]: LastMapTypeSRCResolver + } & { + __typename: SRC['__resolve'][k]['__typename'] + }) + }> + : + never; + +export type MapInterface = SRC extends { + __interface: infer INTERFACE; + __resolve: Record & infer IMPLEMENTORS; + } + ? + (MapResolve extends never ? {} : MapResolve) & { + [k in (keyof SRC['__interface'] & keyof DST)]: LastMapTypeSRCResolver +} : never; + +export type ValueToUnion = T extends { + __typename: infer R; +} + ? { + [P in keyof Omit]: T[P] & { + __typename: R; + }; + } + : T; + +export type ObjectToUnion = { + [P in keyof T]: T[P]; +}[keyof T]; + +type Anify = { [P in keyof T]?: any }; + + +type LastMapTypeSRCResolver = SRC extends undefined + ? undefined + : SRC extends Array + ? LastMapTypeSRCResolver[] + : SRC extends { __interface: any; __resolve: any } + ? MapInterface + : SRC extends { __union: any; __resolve: infer RESOLVE } + ? ObjectToUnion>> + : DST extends boolean + ? SRC + : MapType; + +export type MapType, DST> = DST extends boolean + ? SRC + : DST extends { + __alias: any; + } + ? { + [A in keyof DST["__alias"]]: Required extends Anify< + DST["__alias"][A] + > + ? MapType, DST["__alias"][A]> + : never; + } & + { + [Key in keyof Omit]: DST[Key] extends [ + any, + infer PAYLOAD + ] + ? LastMapTypeSRCResolver + : LastMapTypeSRCResolver; + } + : { + [Key in keyof DST]: DST[Key] extends [any, infer PAYLOAD] + ? LastMapTypeSRCResolver + : LastMapTypeSRCResolver; + }; + +type OperationToGraphQL = (o: Z | V, variables?: Record) => Promise>; + +type CastToGraphQL = ( + resultOfYourQuery: any +) => (o: Z | V) => MapType; + +type fetchOptions = ArgsType; + +export type SelectionFunction = (t: T | V) => T; +type FetchFunction = (query: string, variables?: Record) => Promise; + + + +export const ZeusSelect = () => ((t: any) => t) as SelectionFunction; + +export const ScalarResolver = (scalar: string, value: any) => { + switch (scalar) { + case 'String': + return `${JSON.stringify(value)}`; + case 'Int': + return `${value}`; + case 'Float': + return `${value}`; + case 'Boolean': + return `${value}`; + case 'ID': + return `"${value}"`; + case 'enum': + return `${value}`; + case 'scalar': + return `${value}`; + default: + return false; + } +}; + + +export const TypesPropsResolver = ({ + value, + type, + name, + key, + blockArrays +}: { + value: any; + type: string; + name: string; + key?: string; + blockArrays?: boolean; +}): string => { + if (value === null) { + return `null`; + } + let resolvedValue = AllTypesProps[type][name]; + if (key) { + resolvedValue = resolvedValue[key]; + } + if (!resolvedValue) { + throw new Error(`Cannot resolve ${type} ${name}${key ? ` ${key}` : ''}`) + } + const typeResolved = resolvedValue.type; + const isArray = resolvedValue.array; + const isArrayRequired = resolvedValue.arrayRequired; + if (typeof value === 'string' && value.startsWith(`ZEUS_VAR$`)) { + const isRequired = resolvedValue.required ? '!' : ''; + let t = `${typeResolved}`; + if (isArray) { + if (isRequired) { + t = `${t}!`; + } + t = `[${t}]`; + if(isArrayRequired){ + t = `${t}!`; + } + }else{ + if (isRequired) { + t = `${t}!`; + } + } + return `\$${value.split(`ZEUS_VAR$`)[1]}__ZEUS_VAR__${t}`; + } + if (isArray && !blockArrays) { + return `[${value + .map((v: any) => TypesPropsResolver({ value: v, type, name, key, blockArrays: true })) + .join(',')}]`; + } + const reslovedScalar = ScalarResolver(typeResolved, value); + if (!reslovedScalar) { + const resolvedType = AllTypesProps[typeResolved]; + if (typeof resolvedType === 'object') { + const argsKeys = Object.keys(resolvedType); + return `{${argsKeys + .filter((ak) => value[ak] !== undefined) + .map( + (ak) => `${ak}:${TypesPropsResolver({ value: value[ak], type: typeResolved, name: ak })}` + )}}`; + } + return ScalarResolver(AllTypesProps[typeResolved], value) as string; + } + return reslovedScalar; +}; + + +const isArrayFunction = ( + parent: string[], + a: any[] +) => { + const [values, r] = a; + const [mainKey, key, ...keys] = parent; + const keyValues = Object.keys(values).filter((k) => typeof values[k] !== 'undefined'); + + if (!keys.length) { + return keyValues.length > 0 + ? `(${keyValues + .map( + (v) => + `${v}:${TypesPropsResolver({ + value: values[v], + type: mainKey, + name: key, + key: v + })}` + ) + .join(',')})${r ? traverseToSeekArrays(parent, r) : ''}` + : traverseToSeekArrays(parent, r); + } + + const [typeResolverKey] = keys.splice(keys.length - 1, 1); + let valueToResolve = ReturnTypes[mainKey][key]; + for (const k of keys) { + valueToResolve = ReturnTypes[valueToResolve][k]; + } + + const argumentString = + keyValues.length > 0 + ? `(${keyValues + .map( + (v) => + `${v}:${TypesPropsResolver({ + value: values[v], + type: valueToResolve, + name: typeResolverKey, + key: v + })}` + ) + .join(',')})${r ? traverseToSeekArrays(parent, r) : ''}` + : traverseToSeekArrays(parent, r); + return argumentString; +}; + + +const resolveKV = (k: string, v: boolean | string | { [x: string]: boolean | string }) => + typeof v === 'boolean' ? k : typeof v === 'object' ? `${k}{${objectToTree(v)}}` : `${k}${v}`; + + +const objectToTree = (o: { [x: string]: boolean | string }): string => + `{${Object.keys(o).map((k) => `${resolveKV(k, o[k])}`).join(' ')}}`; + + +const traverseToSeekArrays = (parent: string[], a?: any): string => { + if (!a) return ''; + if (Object.keys(a).length === 0) { + return ''; + } + let b: Record = {}; + if (Array.isArray(a)) { + return isArrayFunction([...parent], a); + } else { + if (typeof a === 'object') { + Object.keys(a) + .filter((k) => typeof a[k] !== 'undefined') + .map((k) => { + if (k === '__alias') { + Object.keys(a[k]).map((aliasKey) => { + const aliasOperations = a[k][aliasKey]; + const aliasOperationName = Object.keys(aliasOperations)[0]; + const aliasOperation = aliasOperations[aliasOperationName]; + b[ + `${aliasOperationName}__alias__${aliasKey}: ${aliasOperationName}` + ] = traverseToSeekArrays([...parent, aliasOperationName], aliasOperation); + }); + } else { + b[k] = traverseToSeekArrays([...parent, k], a[k]); + } + }); + } else { + return ''; + } + } + return objectToTree(b); +}; + + +const buildQuery = (type: string, a?: Record) => + traverseToSeekArrays([type], a); + + +const inspectVariables = (query: string) => { + const regex = /\$\b\w*__ZEUS_VAR__\[?[^!^\]^\s^,^\)^\}]*[!]?[\]]?[!]?/g; + let result; + const AllVariables: string[] = []; + while ((result = regex.exec(query))) { + if (AllVariables.includes(result[0])) { + continue; + } + AllVariables.push(result[0]); + } + if (!AllVariables.length) { + return query; + } + let filteredQuery = query; + AllVariables.forEach((variable) => { + while (filteredQuery.includes(variable)) { + filteredQuery = filteredQuery.replace(variable, variable.split('__ZEUS_VAR__')[0]); + } + }); + return `(${AllVariables.map((a) => a.split('__ZEUS_VAR__')) + .map(([variableName, variableType]) => `${variableName}:${variableType}`) + .join(', ')})${filteredQuery}`; +}; + + +const queryConstruct = (t: 'query' | 'mutation' | 'subscription', tName: string) => (o: Record) => + `${t.toLowerCase()}${inspectVariables(buildQuery(tName, o))}`; + + +const fullChainConstruct = (fn: FetchFunction) => (t: 'query' | 'mutation' | 'subscription', tName: string) => ( + o: Record, + variables?: Record, +) => fn(queryConstruct(t, tName)(o), variables).then((r:any) => { + seekForAliases(r) + return r +}); + + +const seekForAliases = (response: any) => { + const traverseAlias = (value: any) => { + if (Array.isArray(value)) { + value.forEach(seekForAliases); + } else { + if (typeof value === 'object') { + seekForAliases(value); + } + } + }; + if (typeof response === 'object' && response) { + const keys = Object.keys(response); + if (keys.length < 1) { + return; + } + keys.forEach((k) => { + const value = response[k]; + if (k.indexOf('__alias__') !== -1) { + const [operation, alias] = k.split('__alias__'); + response[alias] = { + [operation]: value, + }; + delete response[k]; + } + traverseAlias(value); + }); + } +}; + + +export const $ = (t: TemplateStringsArray): any => `ZEUS_VAR$${t.join('')}`; + + +const handleFetchResponse = ( + response: Parameters['then']>[0], Function>>[0] +): Promise => { + if (!response.ok) { + return new Promise((_, reject) => { + response.text().then(text => { + try { reject(JSON.parse(text)); } + catch (err) { reject(text); } + }).catch(reject); + }); + } + return response.json(); +}; + +const apiFetch = (options: fetchOptions) => (query: string, variables: Record = {}) => { + let fetchFunction = fetch; + let queryString = query; + let fetchOptions = options[1] || {}; + if (fetchOptions.method && fetchOptions.method === 'GET') { + queryString = encodeURIComponent(query); + return fetchFunction(`${options[0]}?query=${queryString}`, fetchOptions) + .then(handleFetchResponse) + .then((response: GraphQLResponse) => { + if (response.errors) { + throw new GraphQLError(response); + } + return response.data; + }); + } + return fetchFunction(`${options[0]}`, { + body: JSON.stringify({ query: queryString, variables }), + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + ...fetchOptions + }) + .then(handleFetchResponse) + .then((response: GraphQLResponse) => { + if (response.errors) { + throw new GraphQLError(response); + } + return response.data; + }); + }; + + + +export const Thunder = (fn: FetchFunction) => ({ + query: ((o: any, variables) => + fullChainConstruct(fn)('query', 'Query')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL, +mutation: ((o: any, variables) => + fullChainConstruct(fn)('mutation', 'Mutation')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL +}); + +export const Chain = (...options: fetchOptions) => ({ + query: ((o: any, variables) => + fullChainConstruct(apiFetch(options))('query', 'Query')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL, +mutation: ((o: any, variables) => + fullChainConstruct(apiFetch(options))('mutation', 'Mutation')(o, variables).then( + (response: any) => response + )) as OperationToGraphQL +}); +export const Zeus = { + query: (o:ValueTypes["Query"]) => queryConstruct('query', 'Query')(o), +mutation: (o:ValueTypes["Mutation"]) => queryConstruct('mutation', 'Mutation')(o) +}; +export const Cast = { + query: ((o: any) => (_: any) => o) as CastToGraphQL< + ValueTypes["Query"], + Query +>, +mutation: ((o: any) => (_: any) => o) as CastToGraphQL< + ValueTypes["Mutation"], + Mutation +> +}; +export const Selectors = { + query: ZeusSelect(), +mutation: ZeusSelect() +}; + + +export const Gql = Chain('http://localhost:4000') \ No newline at end of file diff --git a/__tests/app/helpers/zeus/const.ts b/__tests/app/helpers/zeus/const.ts new file mode 100644 index 0000000..b2ec4f7 --- /dev/null +++ b/__tests/app/helpers/zeus/const.ts @@ -0,0 +1,3093 @@ +/* eslint-disable */ + +export const AllTypesProps: Record = { + Query:{ + login:{ + refreshToken:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + creds:{ + type:"LoginInput", + array:false, + arrayRequired:false, + required:false + } + }, + magicLink:{ + input:{ + type:"ResetPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + placesAutocomplete:{ + session:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + input:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + country:{ + type:"CountryCodesComponentEnum", + array:true, + arrayRequired:false, + required:true + } + }, + placesGeocode:{ + session:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + placeId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + placesGeocodeFromAddress:{ + address:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + categoriesGetMany:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + categoriesGetCount:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + listsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + listsGetMany:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + listsGetCount:{ + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceType:{ + type:"ListEnum", + array:false, + arrayRequired:false, + required:true + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + workspacesGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesGetMany:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesGetCount:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesSearchMany:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesGetMine:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + workspacesGetMineCount:{ + geoSearch:{ + type:"GeolocSearchInput", + array:false, + arrayRequired:false, + required:false + }, + geoSearchWithAddress:{ + type:"GeolocAddressSearchInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + dateFrom:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + dateTo:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + hoursFrom:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + hoursTo:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + workspaceTypes:{ + type:"WorkspaceTypeEnum", + array:true, + arrayRequired:false, + required:true + }, + priceMin:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + priceMax:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + } + }, + bookingsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + bookingsGetMany:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + rangeType:{ + type:"RangeType", + array:false, + arrayRequired:false, + required:false + }, + dateTabType:{ + type:"DateTabType", + array:false, + arrayRequired:false, + required:false + }, + dateRange:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:false + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + status:{ + type:"StatusEnum", + array:true, + arrayRequired:false, + required:true + } + }, + bookingsGetCount:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + rangeType:{ + type:"RangeType", + array:false, + arrayRequired:false, + required:false + }, + dateTabType:{ + type:"DateTabType", + array:false, + arrayRequired:false, + required:false + }, + dateRange:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:false + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + status:{ + type:"StatusEnum", + array:true, + arrayRequired:false, + required:true + } + }, + myOrdersGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + myOrdersGetMany:{ + orderStatus:{ + type:"OrderStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + paymentStatus:{ + type:"PaymentStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + myOrdersGetManyCount:{ + orderStatus:{ + type:"OrderStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + paymentStatus:{ + type:"PaymentStatusEnum", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + roomsGetOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsGetMany:{ + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + }, + roomsGetMessages:{ + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + } + }, + DateTime: "String", + AccountTypeEnum: "enum", + AccountGenderEnum: "enum", + AvailableTranslation: "enum", + SettingsTypeEnum: "enum", + cardTypeEnum: "enum", + cardBrandEnum: "enum", + JSONObject: "String", + RessourceEnum: "enum", + LoginInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + ResetPasswordInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + CountryCodesComponentEnum: "enum", + GetArgs:{ + limit:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + skip:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + sort:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + ListEnum: "enum", + WorkspaceModel:{ + getDistFromLocation:{ + longitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + latitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + radius:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + } + }, + getBookings:{ + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:false + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + status:{ + type:"StatusEnum", + array:true, + arrayRequired:false, + required:true + }, + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + rangeType:{ + type:"RangeType", + array:false, + arrayRequired:false, + required:false + }, + dateTabType:{ + type:"DateTabType", + array:false, + arrayRequired:false, + required:false + }, + dateRange:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + _ids:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + search:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + afterCreatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + afterUpdatedAt:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + } + }, + WorkspaceTypeEnum: "enum", + AvailableCurrency: "enum", + ModelLoadersEnum: "enum", + DurationTypeEnum: "enum", + StatusEnum: "enum", + BookingsRessourceEnum: "enum", + RangeType: "enum", + DateTabType: "enum", + DateRangeComponentInput:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + } + }, + GeolocSearchInput:{ + longitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + latitude:{ + type:"Float", + array:false, + arrayRequired:false, + required:true + }, + radius:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + } + }, + GeolocAddressSearchInput:{ + formattedAddress:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + radius:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + } + }, + RoomModel:{ + getMessages:{ + pagination:{ + type:"GetArgs", + array:false, + arrayRequired:false, + required:false + } + } + }, + ProductRessourceEnum: "enum", + CurrencyEnum: "enum", + InvoicingProvider: "enum", + OrderBusinessStatusEnum: "enum", + OrderStatusEnum: "enum", + PaymentProvider: "enum", + PaymentStatusEnum: "enum", + PromoType: "enum", + Mutation:{ + updateMe:{ + input:{ + type:"EditAccountInput", + array:false, + arrayRequired:false, + required:true + } + }, + updateMeEmail:{ + input:{ + type:"NewEmailInput", + array:false, + arrayRequired:false, + required:true + } + }, + updateMePassword:{ + input:{ + type:"NewPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + resetPassword:{ + input:{ + type:"ResetPasswordInput", + array:false, + arrayRequired:false, + required:true + } + }, + registerGuest:{ + otherInfo:{ + type:"EditAccountInput", + array:false, + arrayRequired:false, + required:true + }, + input:{ + type:"LinkEmailInput", + array:false, + arrayRequired:false, + required:true + } + }, + register:{ + input:{ + type:"NewAccountInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsBillingInfosAddOne:{ + input:{ + type:"BillingAddressInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsBillingInfosEditOne:{ + input:{ + type:"BillingAddressInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsBillingInfosDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsPaymentMethodsAddOne:{ + input:{ + type:"PaymentMethodInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsPaymentMethodsMarkasDefault:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsPaymentMethodsDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + accountsFavoritesAddOne:{ + input:{ + type:"AddInput", + array:false, + arrayRequired:false, + required:true + } + }, + accountsFavoritesRemoveOne:{ + input:{ + type:"RemoveInput", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesAddOne:{ + input:{ + type:"NewWorkspaceInput", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesEditOne:{ + input:{ + type:"EditWorkspaceInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + workspacesDeleteOne:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + finaliseCheckoutWithStripe:{ + input:{ + type:"CheckoutInput", + array:false, + arrayRequired:false, + required:true + } + }, + validateOrder:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsCreateOne:{ + input:{ + type:"NewRoomInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsAddPeople:{ + input:{ + type:"AddPeopleToRoomInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsRemovePeople:{ + input:{ + type:"RemovePeopleFromRoomInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsMarkAllMessageAsRead:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + reserveOneBooking:{ + input:{ + type:"BookingNewInputSchema", + array:false, + arrayRequired:false, + required:true + } + }, + roomsAddOneMessage:{ + input:{ + type:"NewMessageInput", + array:false, + arrayRequired:false, + required:true + } + }, + roomsEditOneMessage:{ + input:{ + type:"EditMessageInput", + array:false, + arrayRequired:false, + required:true + }, + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsMarkMessageAsRead:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + roomsDeleteOneMessage:{ + id:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + } + }, + EditAccountInput:{ + userName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + profilePicture:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + phoneNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + gender:{ + type:"AccountGenderEnum", + array:false, + arrayRequired:false, + required:false + }, + birthDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:false + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + settings:{ + type:"SettingsInput", + array:true, + arrayRequired:false, + required:true + } + }, + ImageInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + fileType:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + large:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + medium:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + small:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + AddressInput:{ + number:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + street:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + streetBis:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + floor:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + box:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + zip:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + state:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + city:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + country:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + SettingsInput:{ + type:{ + type:"SettingsTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + email:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + pushNotifications:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + sms:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + NewEmailInput:{ + newEmail:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + NewPasswordInput:{ + oldPassword:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + newPassword:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + newPasswordConfirmation:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + LinkEmailInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + idToken:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + NewAccountInput:{ + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + password:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + passwordConfirmation:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + userName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + profilePicture:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + phoneNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + gender:{ + type:"AccountGenderEnum", + array:false, + arrayRequired:false, + required:false + }, + birthDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:false + }, + language:{ + type:"AvailableTranslation", + array:false, + arrayRequired:false, + required:false + }, + settings:{ + type:"SettingsInput", + array:true, + arrayRequired:false, + required:true + }, + terms:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:true + } + }, + BillingAddressInput:{ + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + address:{ + type:"AddressStrictInput", + array:false, + arrayRequired:false, + required:true + }, + email:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + default:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + }, + customTags:{ + type:"JSONObject", + array:false, + arrayRequired:false, + required:false + }, + company:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + vatNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + fiscalForm:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + AddressStrictInput:{ + number:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + street:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + zip:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + city:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + country:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + streetBis:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + floor:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + box:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + state:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + PaymentMethodInput:{ + sPayMethodId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + nameOnCard:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + default:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + AddInput:{ + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + ressourceType:{ + type:"RessourceEnum", + array:false, + arrayRequired:false, + required:true + } + }, + RemoveInput:{ + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + NewWorkspaceInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + teaser:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + cover:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + extraImages:{ + type:"ImageInput", + array:true, + arrayRequired:false, + required:true + }, + content:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + seo:{ + type:"SEOSimpleInput", + array:false, + arrayRequired:false, + required:false + }, + urls:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + pricingPerHour:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + currency:{ + type:"AvailableCurrency", + array:false, + arrayRequired:false, + required:true + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + minStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:true + }, + maxStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + availability:{ + type:"AvailabilityInput", + array:false, + arrayRequired:false, + required:true + }, + mainImages:{ + type:"MainImage", + array:false, + arrayRequired:false, + required:true + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + }, + address:{ + type:"AddressStrictInput", + array:false, + arrayRequired:false, + required:true + } + }, + SEOSimpleInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + description:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + keywords:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + } + }, + AvailabilityInput:{ + dates:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:true + }, + hours:{ + type:"HoursInput", + array:true, + arrayRequired:false, + required:true + }, + exceptions:{ + type:"DateExceptionComponentInput", + array:true, + arrayRequired:false, + required:true + }, + noWeekend:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + HoursInput:{ + from:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + to:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + DateExceptionComponentInput:{ + dates:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:true + }, + hours:{ + type:"HoursInput", + array:true, + arrayRequired:false, + required:true + }, + allDay:{ + type:"Boolean", + array:false, + arrayRequired:false, + required:false + } + }, + MainImage:{ + main:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + one:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + two:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + three:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + four:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + } + }, + EditWorkspaceInput:{ + title:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + teaser:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + cover:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + thumbnail:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + }, + extraImages:{ + type:"ImageInput", + array:true, + arrayRequired:false, + required:true + }, + content:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + seo:{ + type:"SEOSimpleInput", + array:false, + arrayRequired:false, + required:false + }, + urls:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + workspaceType:{ + type:"WorkspaceTypeEnum", + array:false, + arrayRequired:false, + required:false + }, + pricingPerHour:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + currency:{ + type:"AvailableCurrency", + array:false, + arrayRequired:false, + required:true + }, + maxCapacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + minStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + maxStay:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + availability:{ + type:"AvailabilityInput", + array:false, + arrayRequired:false, + required:false + }, + mainImages:{ + type:"MainImage", + array:false, + arrayRequired:false, + required:true + }, + equipmentIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + featureIds:{ + type:"String", + array:true, + arrayRequired:false, + required:true + }, + address:{ + type:"AddressStrictInput", + array:false, + arrayRequired:false, + required:false + } + }, + CheckoutInput:{ + contactInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + billingInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + token:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + BuyerInfo:{ + firstName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + lastName:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + email:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + company:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + vatNumber:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + address:{ + type:"AddressInput", + array:false, + arrayRequired:false, + required:true + } + }, + NewRoomInput:{ + accountIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + } + }, + AddPeopleToRoomInput:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + accountIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + } + }, + RemovePeopleFromRoomInput:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + accountIds:{ + type:"String", + array:true, + arrayRequired:true, + required:true + } + }, + BookingNewInputSchema:{ + durationType:{ + type:"DurationTypeEnum", + array:false, + arrayRequired:false, + required:true + }, + startToEnd:{ + type:"DateRangeComponentInput", + array:false, + arrayRequired:false, + required:false + }, + slot:{ + type:"SlotComponentInput", + array:false, + arrayRequired:false, + required:false + }, + capacity:{ + type:"Int", + array:false, + arrayRequired:false, + required:false + }, + comments:{ + type:"String", + array:false, + arrayRequired:false, + required:false + }, + ressourceModel:{ + type:"BookingsRessourceEnum", + array:false, + arrayRequired:false, + required:true + }, + ressourceId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + checkoutInfo:{ + type:"CheckoutEngineInputI", + array:false, + arrayRequired:false, + required:false + } + }, + SlotComponentInput:{ + startDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + }, + endDate:{ + type:"DateTime", + array:false, + arrayRequired:false, + required:true + }, + startTime:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + endTime:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + }, + CheckoutEngineInputI:{ + contactInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + billingInfo:{ + type:"BuyerInfo", + array:false, + arrayRequired:false, + required:true + }, + token:{ + type:"String", + array:false, + arrayRequired:false, + required:false + } + }, + NewMessageInput:{ + roomId:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + message:{ + type:"String", + array:false, + arrayRequired:false, + required:true + }, + file:{ + type:"ImageInput", + array:false, + arrayRequired:false, + required:false + } + }, + EditMessageInput:{ + message:{ + type:"String", + array:false, + arrayRequired:false, + required:true + } + } +} + +export const ReturnTypes: Record = { + Query:{ + me:"AccountModel", + login:"FirebaseTokenResult", + magicLink:"SimpleResult", + placesAutocomplete:"JSONObject", + placesGeocode:"JSONObject", + placesGeocodeFromAddress:"JSONObject", + categoriesGetOne:"CategoryModel", + categoriesGetMany:"CategoryModel", + categoriesGetCount:"Float", + listsGetOne:"ListModel", + listsGetMany:"ListModel", + listsGetCount:"Float", + accountsDashboardLinkStripe:"String", + workspacesGetOne:"WorkspaceModel", + workspacesGetMany:"WorkspaceModel", + workspacesGetCount:"Float", + workspacesSearchMany:"WorkspaceModel", + workspacesGetMine:"WorkspaceModel", + workspacesGetMineCount:"Float", + bookingsGetOne:"BookingEngineSchema", + bookingsGetMany:"BookingEngineSchema", + bookingsGetCount:"Float", + myCurrentOrder:"OrderEngineSchema", + myOrdersGetOne:"OrderEngineSchema", + myOrdersGetMany:"OrderEngineSchema", + myOrdersGetManyCount:"Float", + roomsGetOne:"RoomModel", + roomsGetMany:"RoomModel", + roomsGetMessages:"MessageModel" + }, + AccountModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + types:"AccountTypeEnum", + organisationIds:"String", + email:"String", + userName:"String", + firstName:"String", + lastName:"String", + profilePicture:"ImageComponent", + phoneNumber:"String", + gender:"AccountGenderEnum", + birthDate:"DateTime", + getAge:"Int", + address:"AddressComponent", + language:"AvailableTranslation", + settings:"SettingsComponent", + securityCheck:"Boolean", + paymentInfo:"AccountPaymentInfo", + payoutInfo:"AccountPayoutInfo", + terms:"Boolean", + favLikes:"FavoriteComponent", + getFavorites:"FavoriteComponent" + }, + ImageComponent:{ + title:"String", + fileType:"String", + large:"String", + medium:"String", + small:"String" + }, + AddressComponent:{ + number:"String", + street:"String", + streetBis:"String", + floor:"String", + box:"String", + zip:"String", + state:"String", + city:"String", + country:"String" + }, + SettingsComponent:{ + type:"SettingsTypeEnum", + email:"Boolean", + pushNotifications:"Boolean", + sms:"Boolean" + }, + AccountPaymentInfo:{ + stripeInfo:"StripeInfo", + paymentMethods:"PaymentMethodDetail", + billingInfos:"BillingAddressComponent" + }, + StripeInfo:{ + customerId:"String" + }, + PaymentMethodDetail:{ + _id:"String", + sPayMethodId:"String", + default:"Boolean", + nameOnCard:"String", + type:"cardTypeEnum", + cardInfo:"CardInfo" + }, + CardInfo:{ + brand:"cardBrandEnum", + country:"String", + exp_month:"Int", + exp_year:"Int", + last4:"String" + }, + BillingAddressComponent:{ + firstName:"String", + lastName:"String", + address:"AddressStrictComponent", + email:"String", + default:"Boolean", + customTags:"JSONObject", + company:"String", + vatNumber:"String", + fiscalForm:"String", + _id:"String" + }, + AddressStrictComponent:{ + number:"String", + street:"String", + zip:"String", + city:"String", + country:"String", + streetBis:"String", + floor:"String", + box:"String", + state:"String" + }, + AccountPayoutInfo:{ + stripeInfo:"StripePayoutInfo" + }, + StripePayoutInfo:{ + accountId:"String", + chargesEnabled:"Boolean", + payoutsEnabled:"Boolean", + detailsSubmitted:"Boolean" + }, + FavoriteComponent:{ + ressourceId:"String", + ressourceType:"RessourceEnum", + addedAt:"DateTime", + favorite:"Boolean", + liked:"Boolean" + }, + FirebaseTokenResult:{ + localId:"String", + email:"String", + displayName:"String", + idToken:"String", + registered:"Boolean", + refreshToken:"String", + expiresIn:"String" + }, + SimpleResult:{ + message:"String" + }, + CategoryModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"TranslatableComponent", + teaser:"TranslatableComponent", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"TranslatableComponent", + seo:"SEOField", + urls:"TranslatableComponent", + ressourceType:"RessourceEnum", + colorCode:"String" + }, + TranslatableComponent:{ + en:"String", + fr:"String", + nl:"String", + de:"String" + }, + SEOField:{ + title:"TranslatableComponent", + description:"TranslatableComponent", + keywords:"TranslatableComponent", + thumbnail:"ImageComponent" + }, + ListModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"TranslatableComponent", + teaser:"TranslatableComponent", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"TranslatableComponent", + seo:"SEOField", + urls:"TranslatableComponent", + ressourceType:"ListEnum", + colorCode:"String" + }, + WorkspaceModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"String", + teaser:"String", + cover:"ImageComponent", + thumbnail:"ImageComponent", + extraImages:"ImageComponent", + content:"String", + seo:"SEOSimpleField", + urls:"String", + place:"PlaceComponent", + dist:"GeolocDistComponent", + getDistFromLocation:"Float", + workspaceType:"WorkspaceTypeEnum", + pricingPerHour:"Int", + currency:"AvailableCurrency", + maxCapacity:"Int", + minStay:"Int", + maxStay:"Int", + availability:"AvailabilityComponent", + mainImages:"MainImageComponent", + equipmentIds:"String", + featureIds:"String", + address:"AddressComponent", + finalPrice:"Float", + getEquipments:"ListModel", + getFeatures:"ListModel", + getBookings:"BookingSchema", + getOwner:"WorkspaceOwnerComponent" + }, + SEOSimpleField:{ + title:"String", + description:"String", + keywords:"String", + thumbnail:"ImageComponent" + }, + PlaceComponent:{ + placeId:"String", + address:"AddressComponent", + loc:"LocComponent", + formattedAddress:"String" + }, + LocComponent:{ + type:"String", + coordinates:"Float" + }, + GeolocDistComponent:{ + calculated:"Float", + location:"LocComponent" + }, + AvailabilityComponent:{ + dates:"DateRangeComponent", + hours:"HourComponent", + exceptions:"DateExceptionComponent", + noWeekend:"Boolean" + }, + DateRangeComponent:{ + startDate:"DateTime", + endDate:"DateTime" + }, + HourComponent:{ + from:"String", + to:"String" + }, + DateExceptionComponent:{ + dates:"DateRangeComponent", + hours:"HourComponent", + allDay:"Boolean" + }, + MainImageComponent:{ + main:"ImageComponent", + one:"ImageComponent", + two:"ImageComponent", + three:"ImageComponent", + four:"ImageComponent" + }, + BookingSchema:{ + durationType:"DurationTypeEnum", + startToEnd:"DateRangeComponent", + slot:"SlotComponent", + capacity:"Int", + comments:"String", + status:"StatusEnum", + paths:"EnginePathComponent", + ownerId:"String", + dates:"DateRangeComponent", + _id:"ID", + organisationId:"ID", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime", + tagsIds:"String" + }, + IEngineSchema:{ + "...on BookingSchema": "BookingSchema", + "...on BookingEngineSchema": "BookingEngineSchema", + "...on OrderEngineSchema": "OrderEngineSchema", + _id:"ID", + organisationId:"ID", + paths:"EnginePathComponent", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime", + tagsIds:"String" + }, + EnginePathComponent:{ + ressourceModel:"ModelLoadersEnum", + ressourceId:"String" + }, + MetaBy:{ + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID" + }, + MetaPermissions:{ + r:"String", + w:"String", + d:"String" + }, + SlotComponent:{ + startDate:"DateTime", + endDate:"DateTime", + startTime:"String", + endTime:"String" + }, + WorkspaceOwnerComponent:{ + firstName:"String", + lastName:"String" + }, + BookingEngineSchema:{ + durationType:"DurationTypeEnum", + startToEnd:"DateRangeComponent", + slot:"SlotComponent", + capacity:"Int", + comments:"String", + status:"StatusEnum", + paths:"EnginePathComponent", + ownerId:"String", + dates:"DateRangeComponent", + owner:"AccountModel", + _id:"ID", + organisationId:"ID", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime", + tagsIds:"String", + workspace:"WorkspaceModel", + room:"RoomModel" + }, + RoomModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + title:"String", + stats:"RoomsStats", + getAccounts:"AccountModel", + getMessages:"MessageModel", + lastMessage:"DateTime" + }, + RoomsStats:{ + unreadCounts:"UnreadCount" + }, + UnreadCount:{ + accountId:"String", + count:"Float" + }, + MessageModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + roomId:"String", + notified:"Boolean", + message:"String", + edited:"Boolean", + deleted:"Boolean", + file:"ImageComponent", + sentBy:"String", + readBy:"MessageReadBy" + }, + MessageReadBy:{ + accountId:"String", + readAt:"DateTime" + }, + OrderEngineSchema:{ + lines:"LineItemSchema", + localeInfo:"LocaleInfo", + currency:"CurrencyEnum", + vatExempt:"Boolean", + accountId:"String", + contactInfo:"BuyerInfoComponent", + billingInfo:"BuyerInfoComponent", + promoId:"ID", + invoiceInfo:"InvoiceInfo", + providerOrderItems:"ProviderSchema", + orderName:"TranslatableComponent", + orderStatus:"OrderStatusEnum", + paymentIntent:"PaymentIntentInfo", + paymentStatus:"PaymentStatusEnum", + paymentInfo:"PaymentInfo", + subTotalPrice:"Float", + vatPrice:"Float", + finalPrice:"Float", + totalPrice:"Float", + promo:"PromoModel", + _id:"ID", + organisationId:"ID", + paths:"EnginePathComponent", + by:"MetaBy", + permissions:"MetaPermissions", + createdAt:"DateTime", + updatedAt:"DateTime", + tagsIds:"String", + getBookingInput:"BookingNewInput" + }, + LineItemSchema:{ + _id:"String", + productRessource:"ProductRessourceEnum", + productId:"String", + title:"String", + price:"Float", + salesPrice:"Float", + quantity:"Float", + vatClassId:"String", + parentId:"String", + addOns:"AddOnComponent", + localeInfo:"LocaleInfo", + promoId:"String", + finalPrice:"Float", + getProduct:"ProductUnion", + getLinePrice:"Float", + getLineVatPrice:"Float" + }, + AddOnComponent:{ + addOnId:"String", + quantity:"Float" + }, + LocaleInfo:{ + countryCode:"String", + locale:"AvailableTranslation" + }, + ProductUnion:{ + "...on WorkspaceModel":"WorkspaceModel" + }, + BuyerInfoComponent:{ + firstName:"String", + lastName:"String", + email:"String", + company:"String", + vatNumber:"String", + address:"AddressComponent" + }, + InvoiceInfo:{ + provider:"InvoicingProvider", + invoiceId:"String", + invoiceNumber:"Float" + }, + ProviderSchema:{ + _id:"String", + organisationId:"String", + orderStatus:"OrderBusinessStatusEnum", + lines:"LineItemSchema" + }, + PaymentIntentInfo:{ + provider:"PaymentProvider", + stripePaymentIntentData:"StripePaymentIntentData" + }, + StripePaymentIntentData:{ + id:"String", + client_secret:"String", + currency:"String", + customer:"String", + status:"String" + }, + PaymentInfo:{ + provider:"PaymentProvider", + transactionId:"String" + }, + PromoModel:{ + _id:"ID", + organisationId:"ID", + createdBy:"ID", + updatedBy:"ID", + deletedBy:"ID", + createdAt:"DateTime", + updatedAt:"DateTime", + r:"String", + w:"String", + d:"String", + code:"String", + description:"TranslatableComponent", + type:"PromoType", + value:"Float", + validity:"DateTime", + cummulable:"Boolean", + usageLimit:"Float", + usage:"Float" + }, + BookingNewInput:{ + durationType:"DurationTypeEnum", + startToEnd:"DateRangeComponent", + slot:"SlotComponent", + capacity:"Int", + comments:"String", + ressourceModel:"BookingsRessourceEnum", + ressourceId:"String", + checkoutInfo:"CheckoutEngineInput" + }, + CheckoutEngineInput:{ + contactInfo:"BuyerInfoComponent", + billingInfo:"BuyerInfoComponent", + token:"String" + }, + Mutation:{ + updateMe:"AccountModel", + updateMeEmail:"AccountModel", + updateMePassword:"AccountModel", + resetPassword:"SimpleResult", + registerGuest:"AccountModel", + register:"AccountModel", + accountsBillingInfosAddOne:"BillingAddressComponent", + accountsBillingInfosEditOne:"BillingAddressComponent", + accountsBillingInfosDeleteOne:"String", + accountsPaymentMethodsAddOne:"PaymentMethodDetail", + accountsPaymentMethodsMarkasDefault:"PaymentMethodDetail", + accountsPaymentMethodsDeleteOne:"String", + accountsLinkWithStripe:"String", + accountsRefreshInfoStripe:"AccountModel", + accountsFavoritesAddOne:"FavoriteComponent", + accountsFavoritesRemoveOne:"SuccessResponse", + workspacesAddOne:"WorkspaceModel", + workspacesEditOne:"WorkspaceModel", + workspacesDeleteOne:"WorkspaceModel", + finaliseCheckoutWithStripe:"OrderEngineSchema", + removeCurrentOrder:"OrderEngineSchema", + validateOrder:"OrderEngineSchema", + roomsCreateOne:"RoomModel", + roomsAddPeople:"RoomModel", + roomsRemovePeople:"RoomModel", + roomsMarkAllMessageAsRead:"RoomModel", + reserveOneBooking:"OrderEngineSchema", + roomsAddOneMessage:"MessageModel", + roomsEditOneMessage:"MessageModel", + roomsMarkMessageAsRead:"MessageModel", + roomsDeleteOneMessage:"MessageModel" + }, + SuccessResponse:{ + success:"Boolean" + } +} \ No newline at end of file diff --git a/__tests/app/helpers/zeus/index.ts b/__tests/app/helpers/zeus/index.ts new file mode 100644 index 0000000..889f11b --- /dev/null +++ b/__tests/app/helpers/zeus/index.ts @@ -0,0 +1,3420 @@ +/* eslint-disable */ + +import { AllTypesProps, ReturnTypes } from './const'; +type ZEUS_INTERFACES = GraphQLTypes["IEngineSchema"] +type ZEUS_UNIONS = GraphQLTypes["ProductUnion"] + +export type ValueTypes = { + ["Query"]: AliasType<{ + me?:ValueTypes["AccountModel"], +login?: [{ refreshToken?:string, creds?:ValueTypes["LoginInput"]},ValueTypes["FirebaseTokenResult"]], +magicLink?: [{ input:ValueTypes["ResetPasswordInput"]},ValueTypes["SimpleResult"]], +placesAutocomplete?: [{ session:string, input:string, language?:ValueTypes["AvailableTranslation"], country?:ValueTypes["CountryCodesComponentEnum"][]},true], +placesGeocode?: [{ session:string, placeId:string},true], +placesGeocodeFromAddress?: [{ address:string},true], +categoriesGetOne?: [{ id:string},ValueTypes["CategoryModel"]], +categoriesGetMany?: [{ search?:string, ressourceType:ValueTypes["RessourceEnum"], pagination?:ValueTypes["GetArgs"]},ValueTypes["CategoryModel"]], +categoriesGetCount?: [{ search?:string, ressourceType:ValueTypes["RessourceEnum"], pagination?:ValueTypes["GetArgs"]},true], +listsGetOne?: [{ id:string},ValueTypes["ListModel"]], +listsGetMany?: [{ search?:string, ressourceType:ValueTypes["ListEnum"], pagination?:ValueTypes["GetArgs"]},ValueTypes["ListModel"]], +listsGetCount?: [{ search?:string, ressourceType:ValueTypes["ListEnum"], pagination?:ValueTypes["GetArgs"]},true], + accountsDashboardLinkStripe?:true, +workspacesGetOne?: [{ id:string},ValueTypes["WorkspaceModel"]], +workspacesGetMany?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},ValueTypes["WorkspaceModel"]], +workspacesGetCount?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},true], +workspacesSearchMany?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},ValueTypes["WorkspaceModel"]], +workspacesGetMine?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},ValueTypes["WorkspaceModel"]], +workspacesGetMineCount?: [{ geoSearch?:ValueTypes["GeolocSearchInput"], geoSearchWithAddress?:ValueTypes["GeolocAddressSearchInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], dateFrom?:ValueTypes["DateTime"], dateTo?:ValueTypes["DateTime"], hoursFrom?:string, hoursTo?:string, workspaceType?:ValueTypes["WorkspaceTypeEnum"], workspaceTypes?:ValueTypes["WorkspaceTypeEnum"][], priceMin?:number, priceMax?:number, maxCapacity?:number, equipmentIds?:string[], featureIds?:string[]},true], +bookingsGetOne?: [{ id:string},ValueTypes["BookingEngineSchema"]], +bookingsGetMany?: [{ startDate?:ValueTypes["DateTime"], endDate?:ValueTypes["DateTime"], rangeType?:ValueTypes["RangeType"], dateTabType?:ValueTypes["DateTabType"], dateRange?:ValueTypes["DateRangeComponentInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], ressourceModel?:ValueTypes["BookingsRessourceEnum"], ressourceId?:string, status?:ValueTypes["StatusEnum"][]},ValueTypes["BookingEngineSchema"]], +bookingsGetCount?: [{ startDate?:ValueTypes["DateTime"], endDate?:ValueTypes["DateTime"], rangeType?:ValueTypes["RangeType"], dateTabType?:ValueTypes["DateTabType"], dateRange?:ValueTypes["DateRangeComponentInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"], ressourceModel?:ValueTypes["BookingsRessourceEnum"], ressourceId?:string, status?:ValueTypes["StatusEnum"][]},true], + myCurrentOrder?:ValueTypes["OrderEngineSchema"], +myOrdersGetOne?: [{ id:string},ValueTypes["OrderEngineSchema"]], +myOrdersGetMany?: [{ orderStatus?:ValueTypes["OrderStatusEnum"], paymentStatus?:ValueTypes["PaymentStatusEnum"], pagination?:ValueTypes["GetArgs"], search?:string},ValueTypes["OrderEngineSchema"]], +myOrdersGetManyCount?: [{ orderStatus?:ValueTypes["OrderStatusEnum"], paymentStatus?:ValueTypes["PaymentStatusEnum"], pagination?:ValueTypes["GetArgs"], search?:string},true], +roomsGetOne?: [{ id:string},ValueTypes["RoomModel"]], +roomsGetMany?: [{ pagination?:ValueTypes["GetArgs"]},ValueTypes["RoomModel"]], +roomsGetMessages?: [{ pagination?:ValueTypes["GetArgs"], id:string},ValueTypes["MessageModel"]], + __typename?: true +}>; + ["AccountModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + types?:true, + organisationIds?:true, + email?:true, + userName?:true, + firstName?:true, + lastName?:true, + profilePicture?:ValueTypes["ImageComponent"], + phoneNumber?:true, + gender?:true, + birthDate?:true, + getAge?:true, + address?:ValueTypes["AddressComponent"], + language?:true, + settings?:ValueTypes["SettingsComponent"], + securityCheck?:true, + paymentInfo?:ValueTypes["AccountPaymentInfo"], + payoutInfo?:ValueTypes["AccountPayoutInfo"], + terms?:true, + favLikes?:ValueTypes["FavoriteComponent"], + getFavorites?:ValueTypes["FavoriteComponent"], + __typename?: true +}>; + /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +["DateTime"]:unknown; + ["AccountTypeEnum"]:AccountTypeEnum; + ["ImageComponent"]: AliasType<{ + title?:true, + fileType?:true, + large?:true, + medium?:true, + small?:true, + __typename?: true +}>; + ["AccountGenderEnum"]:AccountGenderEnum; + ["AddressComponent"]: AliasType<{ + number?:true, + street?:true, + streetBis?:true, + floor?:true, + box?:true, + zip?:true, + state?:true, + city?:true, + country?:true, + __typename?: true +}>; + ["AvailableTranslation"]:AvailableTranslation; + ["SettingsComponent"]: AliasType<{ + type?:true, + email?:true, + pushNotifications?:true, + sms?:true, + __typename?: true +}>; + ["SettingsTypeEnum"]:SettingsTypeEnum; + ["AccountPaymentInfo"]: AliasType<{ + stripeInfo?:ValueTypes["StripeInfo"], + paymentMethods?:ValueTypes["PaymentMethodDetail"], + billingInfos?:ValueTypes["BillingAddressComponent"], + __typename?: true +}>; + ["StripeInfo"]: AliasType<{ + customerId?:true, + __typename?: true +}>; + ["PaymentMethodDetail"]: AliasType<{ + _id?:true, + sPayMethodId?:true, + default?:true, + nameOnCard?:true, + type?:true, + cardInfo?:ValueTypes["CardInfo"], + __typename?: true +}>; + ["cardTypeEnum"]:cardTypeEnum; + ["CardInfo"]: AliasType<{ + brand?:true, + country?:true, + exp_month?:true, + exp_year?:true, + last4?:true, + __typename?: true +}>; + ["cardBrandEnum"]:cardBrandEnum; + ["BillingAddressComponent"]: AliasType<{ + firstName?:true, + lastName?:true, + address?:ValueTypes["AddressStrictComponent"], + email?:true, + default?:true, + customTags?:true, + company?:true, + vatNumber?:true, + fiscalForm?:true, + _id?:true, + __typename?: true +}>; + ["AddressStrictComponent"]: AliasType<{ + number?:true, + street?:true, + zip?:true, + city?:true, + country?:true, + streetBis?:true, + floor?:true, + box?:true, + state?:true, + __typename?: true +}>; + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +["JSONObject"]:unknown; + ["AccountPayoutInfo"]: AliasType<{ + stripeInfo?:ValueTypes["StripePayoutInfo"], + __typename?: true +}>; + ["StripePayoutInfo"]: AliasType<{ + accountId?:true, + chargesEnabled?:true, + payoutsEnabled?:true, + detailsSubmitted?:true, + __typename?: true +}>; + ["FavoriteComponent"]: AliasType<{ + ressourceId?:true, + ressourceType?:true, + addedAt?:true, + favorite?:true, + liked?:true, + __typename?: true +}>; + ["RessourceEnum"]:RessourceEnum; + ["FirebaseTokenResult"]: AliasType<{ + localId?:true, + email?:true, + displayName?:true, + idToken?:true, + registered?:true, + refreshToken?:true, + expiresIn?:true, + __typename?: true +}>; + ["LoginInput"]: { + email:string, + password:string +}; + ["SimpleResult"]: AliasType<{ + message?:true, + __typename?: true +}>; + ["ResetPasswordInput"]: { + email:string +}; + ["CountryCodesComponentEnum"]:CountryCodesComponentEnum; + ["CategoryModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:ValueTypes["TranslatableComponent"], + teaser?:ValueTypes["TranslatableComponent"], + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:ValueTypes["TranslatableComponent"], + seo?:ValueTypes["SEOField"], + urls?:ValueTypes["TranslatableComponent"], + ressourceType?:true, + colorCode?:true, + __typename?: true +}>; + ["TranslatableComponent"]: AliasType<{ + en?:true, + fr?:true, + nl?:true, + de?:true, + __typename?: true +}>; + ["SEOField"]: AliasType<{ + title?:ValueTypes["TranslatableComponent"], + description?:ValueTypes["TranslatableComponent"], + keywords?:ValueTypes["TranslatableComponent"], + thumbnail?:ValueTypes["ImageComponent"], + __typename?: true +}>; + ["GetArgs"]: { + limit:number, + skip:number, + sort?:string +}; + ["ListModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:ValueTypes["TranslatableComponent"], + teaser?:ValueTypes["TranslatableComponent"], + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:ValueTypes["TranslatableComponent"], + seo?:ValueTypes["SEOField"], + urls?:ValueTypes["TranslatableComponent"], + ressourceType?:true, + colorCode?:true, + __typename?: true +}>; + ["ListEnum"]:ListEnum; + ["WorkspaceModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:true, + teaser?:true, + cover?:ValueTypes["ImageComponent"], + thumbnail?:ValueTypes["ImageComponent"], + extraImages?:ValueTypes["ImageComponent"], + content?:true, + seo?:ValueTypes["SEOSimpleField"], + urls?:true, + place?:ValueTypes["PlaceComponent"], + dist?:ValueTypes["GeolocDistComponent"], +getDistFromLocation?: [{ longitude:number, latitude:number, radius?:number},true], + workspaceType?:true, + pricingPerHour?:true, + currency?:true, + maxCapacity?:true, + minStay?:true, + maxStay?:true, + availability?:ValueTypes["AvailabilityComponent"], + mainImages?:ValueTypes["MainImageComponent"], + equipmentIds?:true, + featureIds?:true, + address?:ValueTypes["AddressComponent"], + finalPrice?:true, + getEquipments?:ValueTypes["ListModel"], + getFeatures?:ValueTypes["ListModel"], +getBookings?: [{ ressourceModel?:ValueTypes["BookingsRessourceEnum"], ressourceId?:string, status?:ValueTypes["StatusEnum"][], startDate?:ValueTypes["DateTime"], endDate?:ValueTypes["DateTime"], rangeType?:ValueTypes["RangeType"], dateTabType?:ValueTypes["DateTabType"], dateRange?:ValueTypes["DateRangeComponentInput"], _ids?:string[], search?:string, afterCreatedAt?:ValueTypes["DateTime"], afterUpdatedAt?:ValueTypes["DateTime"], pagination?:ValueTypes["GetArgs"]},ValueTypes["BookingSchema"]], + getOwner?:ValueTypes["WorkspaceOwnerComponent"], + __typename?: true +}>; + ["SEOSimpleField"]: AliasType<{ + title?:true, + description?:true, + keywords?:true, + thumbnail?:ValueTypes["ImageComponent"], + __typename?: true +}>; + ["PlaceComponent"]: AliasType<{ + placeId?:true, + address?:ValueTypes["AddressComponent"], + loc?:ValueTypes["LocComponent"], + formattedAddress?:true, + __typename?: true +}>; + ["LocComponent"]: AliasType<{ + type?:true, + coordinates?:true, + __typename?: true +}>; + ["GeolocDistComponent"]: AliasType<{ + calculated?:true, + location?:ValueTypes["LocComponent"], + __typename?: true +}>; + ["WorkspaceTypeEnum"]:WorkspaceTypeEnum; + ["AvailableCurrency"]:AvailableCurrency; + ["AvailabilityComponent"]: AliasType<{ + dates?:ValueTypes["DateRangeComponent"], + hours?:ValueTypes["HourComponent"], + exceptions?:ValueTypes["DateExceptionComponent"], + noWeekend?:true, + __typename?: true +}>; + ["DateRangeComponent"]: AliasType<{ + startDate?:true, + endDate?:true, + __typename?: true +}>; + ["HourComponent"]: AliasType<{ + from?:true, + to?:true, + __typename?: true +}>; + ["DateExceptionComponent"]: AliasType<{ + dates?:ValueTypes["DateRangeComponent"], + hours?:ValueTypes["HourComponent"], + allDay?:true, + __typename?: true +}>; + ["MainImageComponent"]: AliasType<{ + main?:ValueTypes["ImageComponent"], + one?:ValueTypes["ImageComponent"], + two?:ValueTypes["ImageComponent"], + three?:ValueTypes["ImageComponent"], + four?:ValueTypes["ImageComponent"], + __typename?: true +}>; + ["BookingSchema"]: AliasType<{ + durationType?:true, + startToEnd?:ValueTypes["DateRangeComponent"], + slot?:ValueTypes["SlotComponent"], + capacity?:true, + comments?:true, + status?:true, + paths?:ValueTypes["EnginePathComponent"], + ownerId?:true, + dates?:ValueTypes["DateRangeComponent"], + _id?:true, + organisationId?:true, + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + tagsIds?:true, + __typename?: true +}>; + ["IEngineSchema"]:AliasType<{ + _id?:true, + organisationId?:true, + paths?:ValueTypes["EnginePathComponent"], + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + tagsIds?:true; + ['...on BookingSchema']?: Omit; + ['...on BookingEngineSchema']?: Omit; + ['...on OrderEngineSchema']?: Omit; + __typename?: true +}>; + ["EnginePathComponent"]: AliasType<{ + ressourceModel?:true, + ressourceId?:true, + __typename?: true +}>; + ["ModelLoadersEnum"]:ModelLoadersEnum; + ["MetaBy"]: AliasType<{ + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + __typename?: true +}>; + ["MetaPermissions"]: AliasType<{ + r?:true, + w?:true, + d?:true, + __typename?: true +}>; + ["DurationTypeEnum"]:DurationTypeEnum; + ["SlotComponent"]: AliasType<{ + startDate?:true, + endDate?:true, + startTime?:true, + endTime?:true, + __typename?: true +}>; + ["StatusEnum"]:StatusEnum; + ["BookingsRessourceEnum"]:BookingsRessourceEnum; + ["RangeType"]:RangeType; + ["DateTabType"]:DateTabType; + ["DateRangeComponentInput"]: { + startDate:ValueTypes["DateTime"], + endDate:ValueTypes["DateTime"] +}; + ["WorkspaceOwnerComponent"]: AliasType<{ + firstName?:true, + lastName?:true, + __typename?: true +}>; + ["GeolocSearchInput"]: { + longitude:number, + latitude:number, + radius?:number +}; + ["GeolocAddressSearchInput"]: { + formattedAddress:string, + radius?:number +}; + ["BookingEngineSchema"]: AliasType<{ + durationType?:true, + startToEnd?:ValueTypes["DateRangeComponent"], + slot?:ValueTypes["SlotComponent"], + capacity?:true, + comments?:true, + status?:true, + paths?:ValueTypes["EnginePathComponent"], + ownerId?:true, + dates?:ValueTypes["DateRangeComponent"], + owner?:ValueTypes["AccountModel"], + _id?:true, + organisationId?:true, + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + tagsIds?:true, + workspace?:ValueTypes["WorkspaceModel"], + room?:ValueTypes["RoomModel"], + __typename?: true +}>; + ["RoomModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + title?:true, + stats?:ValueTypes["RoomsStats"], + getAccounts?:ValueTypes["AccountModel"], +getMessages?: [{ pagination?:ValueTypes["GetArgs"]},ValueTypes["MessageModel"]], + lastMessage?:true, + __typename?: true +}>; + ["RoomsStats"]: AliasType<{ + unreadCounts?:ValueTypes["UnreadCount"], + __typename?: true +}>; + ["UnreadCount"]: AliasType<{ + accountId?:true, + count?:true, + __typename?: true +}>; + ["MessageModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + roomId?:true, + notified?:true, + message?:true, + edited?:true, + deleted?:true, + file?:ValueTypes["ImageComponent"], + sentBy?:true, + readBy?:ValueTypes["MessageReadBy"], + __typename?: true +}>; + ["MessageReadBy"]: AliasType<{ + accountId?:true, + readAt?:true, + __typename?: true +}>; + ["OrderEngineSchema"]: AliasType<{ + lines?:ValueTypes["LineItemSchema"], + localeInfo?:ValueTypes["LocaleInfo"], + currency?:true, + vatExempt?:true, + accountId?:true, + contactInfo?:ValueTypes["BuyerInfoComponent"], + billingInfo?:ValueTypes["BuyerInfoComponent"], + promoId?:true, + invoiceInfo?:ValueTypes["InvoiceInfo"], + providerOrderItems?:ValueTypes["ProviderSchema"], + orderName?:ValueTypes["TranslatableComponent"], + orderStatus?:true, + paymentIntent?:ValueTypes["PaymentIntentInfo"], + paymentStatus?:true, + paymentInfo?:ValueTypes["PaymentInfo"], + subTotalPrice?:true, + vatPrice?:true, + finalPrice?:true, + totalPrice?:true, + promo?:ValueTypes["PromoModel"], + _id?:true, + organisationId?:true, + paths?:ValueTypes["EnginePathComponent"], + by?:ValueTypes["MetaBy"], + permissions?:ValueTypes["MetaPermissions"], + createdAt?:true, + updatedAt?:true, + tagsIds?:true, + getBookingInput?:ValueTypes["BookingNewInput"], + __typename?: true +}>; + ["LineItemSchema"]: AliasType<{ + _id?:true, + productRessource?:true, + productId?:true, + title?:true, + price?:true, + salesPrice?:true, + quantity?:true, + vatClassId?:true, + parentId?:true, + addOns?:ValueTypes["AddOnComponent"], + localeInfo?:ValueTypes["LocaleInfo"], + promoId?:true, + finalPrice?:true, + getProduct?:ValueTypes["ProductUnion"], + getLinePrice?:true, + getLineVatPrice?:true, + __typename?: true +}>; + ["ProductRessourceEnum"]:ProductRessourceEnum; + ["AddOnComponent"]: AliasType<{ + addOnId?:true, + quantity?:true, + __typename?: true +}>; + ["LocaleInfo"]: AliasType<{ + countryCode?:true, + locale?:true, + __typename?: true +}>; + ["ProductUnion"]: AliasType<{ ["...on WorkspaceModel"] : ValueTypes["WorkspaceModel"] + __typename?: true +}>; + ["CurrencyEnum"]:CurrencyEnum; + ["BuyerInfoComponent"]: AliasType<{ + firstName?:true, + lastName?:true, + email?:true, + company?:true, + vatNumber?:true, + address?:ValueTypes["AddressComponent"], + __typename?: true +}>; + ["InvoiceInfo"]: AliasType<{ + provider?:true, + invoiceId?:true, + invoiceNumber?:true, + __typename?: true +}>; + ["InvoicingProvider"]:InvoicingProvider; + ["ProviderSchema"]: AliasType<{ + _id?:true, + organisationId?:true, + orderStatus?:true, + lines?:ValueTypes["LineItemSchema"], + __typename?: true +}>; + ["OrderBusinessStatusEnum"]:OrderBusinessStatusEnum; + ["OrderStatusEnum"]:OrderStatusEnum; + ["PaymentIntentInfo"]: AliasType<{ + provider?:true, + stripePaymentIntentData?:ValueTypes["StripePaymentIntentData"], + __typename?: true +}>; + ["PaymentProvider"]:PaymentProvider; + ["StripePaymentIntentData"]: AliasType<{ + id?:true, + client_secret?:true, + currency?:true, + customer?:true, + status?:true, + __typename?: true +}>; + ["PaymentStatusEnum"]:PaymentStatusEnum; + ["PaymentInfo"]: AliasType<{ + provider?:true, + transactionId?:true, + __typename?: true +}>; + ["PromoModel"]: AliasType<{ + _id?:true, + organisationId?:true, + createdBy?:true, + updatedBy?:true, + deletedBy?:true, + createdAt?:true, + updatedAt?:true, + r?:true, + w?:true, + d?:true, + code?:true, + description?:ValueTypes["TranslatableComponent"], + type?:true, + value?:true, + validity?:true, + cummulable?:true, + usageLimit?:true, + usage?:true, + __typename?: true +}>; + ["PromoType"]:PromoType; + ["BookingNewInput"]: AliasType<{ + durationType?:true, + startToEnd?:ValueTypes["DateRangeComponent"], + slot?:ValueTypes["SlotComponent"], + capacity?:true, + comments?:true, + ressourceModel?:true, + ressourceId?:true, + checkoutInfo?:ValueTypes["CheckoutEngineInput"], + __typename?: true +}>; + ["CheckoutEngineInput"]: AliasType<{ + contactInfo?:ValueTypes["BuyerInfoComponent"], + billingInfo?:ValueTypes["BuyerInfoComponent"], + token?:true, + __typename?: true +}>; + ["Mutation"]: AliasType<{ +updateMe?: [{ input:ValueTypes["EditAccountInput"]},ValueTypes["AccountModel"]], +updateMeEmail?: [{ input:ValueTypes["NewEmailInput"]},ValueTypes["AccountModel"]], +updateMePassword?: [{ input:ValueTypes["NewPasswordInput"]},ValueTypes["AccountModel"]], +resetPassword?: [{ input:ValueTypes["ResetPasswordInput"]},ValueTypes["SimpleResult"]], +registerGuest?: [{ otherInfo:ValueTypes["EditAccountInput"], input:ValueTypes["LinkEmailInput"]},ValueTypes["AccountModel"]], +register?: [{ input:ValueTypes["NewAccountInput"]},ValueTypes["AccountModel"]], +accountsBillingInfosAddOne?: [{ input:ValueTypes["BillingAddressInput"]},ValueTypes["BillingAddressComponent"]], +accountsBillingInfosEditOne?: [{ input:ValueTypes["BillingAddressInput"], id:string},ValueTypes["BillingAddressComponent"]], +accountsBillingInfosDeleteOne?: [{ id:string},true], +accountsPaymentMethodsAddOne?: [{ input:ValueTypes["PaymentMethodInput"]},ValueTypes["PaymentMethodDetail"]], +accountsPaymentMethodsMarkasDefault?: [{ id:string},ValueTypes["PaymentMethodDetail"]], +accountsPaymentMethodsDeleteOne?: [{ id:string},true], + accountsLinkWithStripe?:true, + accountsRefreshInfoStripe?:ValueTypes["AccountModel"], +accountsFavoritesAddOne?: [{ input:ValueTypes["AddInput"]},ValueTypes["FavoriteComponent"]], +accountsFavoritesRemoveOne?: [{ input:ValueTypes["RemoveInput"]},ValueTypes["SuccessResponse"]], +workspacesAddOne?: [{ input:ValueTypes["NewWorkspaceInput"]},ValueTypes["WorkspaceModel"]], +workspacesEditOne?: [{ input:ValueTypes["EditWorkspaceInput"], id:string},ValueTypes["WorkspaceModel"]], +workspacesDeleteOne?: [{ id:string},ValueTypes["WorkspaceModel"]], +finaliseCheckoutWithStripe?: [{ input:ValueTypes["CheckoutInput"]},ValueTypes["OrderEngineSchema"]], + removeCurrentOrder?:ValueTypes["OrderEngineSchema"], +validateOrder?: [{ id:string},ValueTypes["OrderEngineSchema"]], +roomsCreateOne?: [{ input:ValueTypes["NewRoomInput"]},ValueTypes["RoomModel"]], +roomsAddPeople?: [{ input:ValueTypes["AddPeopleToRoomInput"]},ValueTypes["RoomModel"]], +roomsRemovePeople?: [{ input:ValueTypes["RemovePeopleFromRoomInput"]},ValueTypes["RoomModel"]], +roomsMarkAllMessageAsRead?: [{ roomId:string},ValueTypes["RoomModel"]], +reserveOneBooking?: [{ input:ValueTypes["BookingNewInputSchema"]},ValueTypes["OrderEngineSchema"]], +roomsAddOneMessage?: [{ input:ValueTypes["NewMessageInput"]},ValueTypes["MessageModel"]], +roomsEditOneMessage?: [{ input:ValueTypes["EditMessageInput"], id:string},ValueTypes["MessageModel"]], +roomsMarkMessageAsRead?: [{ id:string},ValueTypes["MessageModel"]], +roomsDeleteOneMessage?: [{ id:string},ValueTypes["MessageModel"]], + __typename?: true +}>; + ["EditAccountInput"]: { + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ValueTypes["ImageInput"], + phoneNumber?:string, + gender?:ValueTypes["AccountGenderEnum"], + birthDate?:ValueTypes["DateTime"], + address?:ValueTypes["AddressInput"], + language?:ValueTypes["AvailableTranslation"], + settings?:ValueTypes["SettingsInput"][] +}; + ["ImageInput"]: { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +}; + ["AddressInput"]: { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +}; + ["SettingsInput"]: { + type:ValueTypes["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +}; + ["NewEmailInput"]: { + newEmail:string +}; + ["NewPasswordInput"]: { + oldPassword:string, + newPassword:string, + newPasswordConfirmation:string +}; + ["LinkEmailInput"]: { + email:string, + password:string, + idToken:string +}; + ["NewAccountInput"]: { + email:string, + password:string, + passwordConfirmation:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ValueTypes["ImageInput"], + phoneNumber?:string, + gender?:ValueTypes["AccountGenderEnum"], + birthDate?:ValueTypes["DateTime"], + address?:ValueTypes["AddressInput"], + language?:ValueTypes["AvailableTranslation"], + settings?:ValueTypes["SettingsInput"][], + terms:boolean +}; + ["BillingAddressInput"]: { + firstName:string, + lastName:string, + address:ValueTypes["AddressStrictInput"], + email?:string, + default?:boolean, + customTags?:ValueTypes["JSONObject"], + company?:string, + vatNumber?:string, + fiscalForm?:string +}; + ["AddressStrictInput"]: { + number:string, + street:string, + zip:string, + city:string, + country:string, + streetBis?:string, + floor?:string, + box?:string, + state?:string +}; + ["PaymentMethodInput"]: { + sPayMethodId:string, + nameOnCard:string, + default?:boolean +}; + ["AddInput"]: { + ressourceId:string, + ressourceType:ValueTypes["RessourceEnum"] +}; + ["SuccessResponse"]: AliasType<{ + success?:true, + __typename?: true +}>; + ["RemoveInput"]: { + ressourceId:string +}; + ["NewWorkspaceInput"]: { + title?:string, + teaser?:string, + cover?:ValueTypes["ImageInput"], + thumbnail?:ValueTypes["ImageInput"], + extraImages?:ValueTypes["ImageInput"][], + content?:string, + seo?:ValueTypes["SEOSimpleInput"], + urls?:string, + workspaceType:ValueTypes["WorkspaceTypeEnum"], + pricingPerHour:number, + currency:ValueTypes["AvailableCurrency"], + maxCapacity:number, + minStay:number, + maxStay?:number, + availability:ValueTypes["AvailabilityInput"], + mainImages:ValueTypes["MainImage"], + equipmentIds:string[], + featureIds:string[], + address:ValueTypes["AddressStrictInput"] +}; + ["SEOSimpleInput"]: { + title:string, + description:string, + keywords?:string, + thumbnail?:ValueTypes["ImageInput"] +}; + ["AvailabilityInput"]: { + dates:ValueTypes["DateRangeComponentInput"], + hours?:ValueTypes["HoursInput"][], + exceptions?:ValueTypes["DateExceptionComponentInput"][], + noWeekend?:boolean +}; + ["HoursInput"]: { + from:string, + to:string +}; + ["DateExceptionComponentInput"]: { + dates:ValueTypes["DateRangeComponentInput"], + hours?:ValueTypes["HoursInput"][], + allDay?:boolean +}; + ["MainImage"]: { + main?:ValueTypes["ImageInput"], + one?:ValueTypes["ImageInput"], + two?:ValueTypes["ImageInput"], + three?:ValueTypes["ImageInput"], + four?:ValueTypes["ImageInput"] +}; + ["EditWorkspaceInput"]: { + title?:string, + teaser?:string, + cover?:ValueTypes["ImageInput"], + thumbnail?:ValueTypes["ImageInput"], + extraImages?:ValueTypes["ImageInput"][], + content?:string, + seo?:ValueTypes["SEOSimpleInput"], + urls?:string, + workspaceType?:ValueTypes["WorkspaceTypeEnum"], + pricingPerHour?:number, + currency:ValueTypes["AvailableCurrency"], + maxCapacity?:number, + minStay?:number, + maxStay?:number, + availability?:ValueTypes["AvailabilityInput"], + mainImages:ValueTypes["MainImage"], + equipmentIds?:string[], + featureIds?:string[], + address?:ValueTypes["AddressStrictInput"] +}; + ["CheckoutInput"]: { + contactInfo:ValueTypes["BuyerInfo"], + billingInfo:ValueTypes["BuyerInfo"], + token?:string +}; + ["BuyerInfo"]: { + firstName:string, + lastName:string, + email:string, + company?:string, + vatNumber?:string, + address:ValueTypes["AddressInput"] +}; + ["NewRoomInput"]: { + accountIds:string[] +}; + ["AddPeopleToRoomInput"]: { + roomId:string, + accountIds:string[] +}; + ["RemovePeopleFromRoomInput"]: { + roomId:string, + accountIds:string[] +}; + ["BookingNewInputSchema"]: { + durationType:ValueTypes["DurationTypeEnum"], + startToEnd?:ValueTypes["DateRangeComponentInput"], + slot?:ValueTypes["SlotComponentInput"], + capacity?:number, + comments?:string, + ressourceModel:ValueTypes["BookingsRessourceEnum"], + ressourceId:string, + checkoutInfo?:ValueTypes["CheckoutEngineInputI"] +}; + ["SlotComponentInput"]: { + startDate:ValueTypes["DateTime"], + endDate:ValueTypes["DateTime"], + startTime:string, + endTime:string +}; + ["CheckoutEngineInputI"]: { + contactInfo:ValueTypes["BuyerInfo"], + billingInfo:ValueTypes["BuyerInfo"], + token?:string +}; + ["NewMessageInput"]: { + roomId:string, + message:string, + file?:ValueTypes["ImageInput"] +}; + ["EditMessageInput"]: { + message:string +} + } + +export type ModelTypes = { + ["Query"]: { + me:ModelTypes["AccountModel"], + login:ModelTypes["FirebaseTokenResult"], + magicLink:ModelTypes["SimpleResult"], + placesAutocomplete:ModelTypes["JSONObject"][], + placesGeocode:ModelTypes["JSONObject"], + placesGeocodeFromAddress:ModelTypes["JSONObject"], + categoriesGetOne:ModelTypes["CategoryModel"], + categoriesGetMany:ModelTypes["CategoryModel"][], + categoriesGetCount:number, + listsGetOne:ModelTypes["ListModel"], + listsGetMany:ModelTypes["ListModel"][], + listsGetCount:number, + accountsDashboardLinkStripe:string, + workspacesGetOne:ModelTypes["WorkspaceModel"], + workspacesGetMany:ModelTypes["WorkspaceModel"][], + workspacesGetCount:number, + workspacesSearchMany:ModelTypes["WorkspaceModel"][], + workspacesGetMine:ModelTypes["WorkspaceModel"][], + workspacesGetMineCount:number, + bookingsGetOne:ModelTypes["BookingEngineSchema"], + bookingsGetMany:ModelTypes["BookingEngineSchema"][], + bookingsGetCount:number, + myCurrentOrder:ModelTypes["OrderEngineSchema"], + myOrdersGetOne:ModelTypes["OrderEngineSchema"], + myOrdersGetMany:ModelTypes["OrderEngineSchema"][], + myOrdersGetManyCount:number, + roomsGetOne:ModelTypes["RoomModel"], + roomsGetMany:ModelTypes["RoomModel"][], + roomsGetMessages:ModelTypes["MessageModel"][] +}; + ["AccountModel"]: { + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + r:string[], + w:string[], + d:string[], + types:ModelTypes["AccountTypeEnum"][], + organisationIds?:string[], + email:string, + userName?:string, + firstName?:string, + lastName?:string, + profilePicture?:ModelTypes["ImageComponent"], + phoneNumber?:string, + gender?:ModelTypes["AccountGenderEnum"], + birthDate?:ModelTypes["DateTime"], + getAge?:number, + address?:ModelTypes["AddressComponent"], + language?:ModelTypes["AvailableTranslation"], + settings?:ModelTypes["SettingsComponent"][], + securityCheck:boolean, + paymentInfo?:ModelTypes["AccountPaymentInfo"], + payoutInfo?:ModelTypes["AccountPayoutInfo"], + terms:boolean, + favLikes?:(ModelTypes["FavoriteComponent"] | undefined)[], + getFavorites:ModelTypes["FavoriteComponent"][] +}; + /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +["DateTime"]:any; + ["AccountTypeEnum"]: GraphQLTypes["AccountTypeEnum"]; + ["ImageComponent"]: { + title?:string, + fileType?:string, + large:string, + medium?:string, + small?:string +}; + ["AccountGenderEnum"]: GraphQLTypes["AccountGenderEnum"]; + ["AddressComponent"]: { + number?:string, + street?:string, + streetBis?:string, + floor?:string, + box?:string, + zip?:string, + state?:string, + city:string, + country:string +}; + ["AvailableTranslation"]: GraphQLTypes["AvailableTranslation"]; + ["SettingsComponent"]: { + type:ModelTypes["SettingsTypeEnum"], + email?:boolean, + pushNotifications?:boolean, + sms?:boolean +}; + ["SettingsTypeEnum"]: GraphQLTypes["SettingsTypeEnum"]; + ["AccountPaymentInfo"]: { + stripeInfo?:ModelTypes["StripeInfo"], + paymentMethods?:ModelTypes["PaymentMethodDetail"][], + billingInfos?:ModelTypes["BillingAddressComponent"][] +}; + ["StripeInfo"]: { + customerId:string +}; + ["PaymentMethodDetail"]: { + _id:string, + sPayMethodId:string, + default:boolean, + nameOnCard:string, + type:ModelTypes["cardTypeEnum"], + cardInfo?:ModelTypes["CardInfo"] +}; + ["cardTypeEnum"]: GraphQLTypes["cardTypeEnum"]; + ["CardInfo"]: { + brand:ModelTypes["cardBrandEnum"], + country:string, + exp_month:number, + exp_year:number, + last4:string +}; + ["cardBrandEnum"]: GraphQLTypes["cardBrandEnum"]; + ["BillingAddressComponent"]: { + firstName:string, + lastName:string, + address:ModelTypes["AddressStrictComponent"], + email?:string, + default?:boolean, + customTags?:ModelTypes["JSONObject"], + company?:string, + vatNumber?:string, + fiscalForm?:string, + _id:string +}; + ["AddressStrictComponent"]: { + number:string, + street:string, + zip:string, + city:string, + country:string, + streetBis?:string, + floor?:string, + box?:string, + state?:string +}; + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +["JSONObject"]:any; + ["AccountPayoutInfo"]: { + stripeInfo?:ModelTypes["StripePayoutInfo"] +}; + ["StripePayoutInfo"]: { + accountId:string, + chargesEnabled:boolean, + payoutsEnabled:boolean, + detailsSubmitted:boolean +}; + ["FavoriteComponent"]: { + ressourceId:string, + ressourceType:ModelTypes["RessourceEnum"], + addedAt:ModelTypes["DateTime"], + favorite?:boolean, + liked?:boolean +}; + ["RessourceEnum"]: GraphQLTypes["RessourceEnum"]; + ["FirebaseTokenResult"]: { + localId:string, + email?:string, + displayName?:string, + idToken:string, + registered?:boolean, + refreshToken:string, + expiresIn:string +}; + ["LoginInput"]: GraphQLTypes["LoginInput"]; + ["SimpleResult"]: { + message:string +}; + ["ResetPasswordInput"]: GraphQLTypes["ResetPasswordInput"]; + ["CountryCodesComponentEnum"]: GraphQLTypes["CountryCodesComponentEnum"]; + ["CategoryModel"]: { + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + r:string[], + w:string[], + d:string[], + title:ModelTypes["TranslatableComponent"], + teaser?:ModelTypes["TranslatableComponent"], + cover?:ModelTypes["ImageComponent"], + thumbnail?:ModelTypes["ImageComponent"], + extraImages?:ModelTypes["ImageComponent"][], + content?:ModelTypes["TranslatableComponent"], + seo?:ModelTypes["SEOField"], + urls?:ModelTypes["TranslatableComponent"], + ressourceType:ModelTypes["RessourceEnum"], + colorCode?:string +}; + ["TranslatableComponent"]: { + en?:string, + fr:string, + nl?:string, + de?:string +}; + ["SEOField"]: { + title:ModelTypes["TranslatableComponent"], + description:ModelTypes["TranslatableComponent"], + keywords?:ModelTypes["TranslatableComponent"], + thumbnail?:ModelTypes["ImageComponent"] +}; + ["GetArgs"]: GraphQLTypes["GetArgs"]; + ["ListModel"]: { + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + r:string[], + w:string[], + d:string[], + title:ModelTypes["TranslatableComponent"], + teaser?:ModelTypes["TranslatableComponent"], + cover?:ModelTypes["ImageComponent"], + thumbnail?:ModelTypes["ImageComponent"], + extraImages?:ModelTypes["ImageComponent"][], + content?:ModelTypes["TranslatableComponent"], + seo?:ModelTypes["SEOField"], + urls?:ModelTypes["TranslatableComponent"], + ressourceType:ModelTypes["ListEnum"], + colorCode?:string +}; + ["ListEnum"]: GraphQLTypes["ListEnum"]; + ["WorkspaceModel"]: { + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + r:string[], + w:string[], + d:string[], + title?:string, + teaser?:string, + cover?:ModelTypes["ImageComponent"], + thumbnail?:ModelTypes["ImageComponent"], + extraImages?:ModelTypes["ImageComponent"][], + content?:string, + seo?:ModelTypes["SEOSimpleField"], + urls?:string, + place:ModelTypes["PlaceComponent"], + dist?:ModelTypes["GeolocDistComponent"], + getDistFromLocation?:number, + workspaceType:ModelTypes["WorkspaceTypeEnum"], + pricingPerHour:number, + currency:ModelTypes["AvailableCurrency"], + maxCapacity:number, + minStay:number, + maxStay?:number, + availability:ModelTypes["AvailabilityComponent"], + mainImages:ModelTypes["MainImageComponent"], + equipmentIds:string[], + featureIds:string[], + address:ModelTypes["AddressComponent"], + finalPrice:number, + getEquipments?:(ModelTypes["ListModel"] | undefined)[], + getFeatures?:(ModelTypes["ListModel"] | undefined)[], + getBookings?:(ModelTypes["BookingSchema"] | undefined)[], + getOwner?:ModelTypes["WorkspaceOwnerComponent"] +}; + ["SEOSimpleField"]: { + title:string, + description:string, + keywords?:string, + thumbnail?:ModelTypes["ImageComponent"] +}; + ["PlaceComponent"]: { + placeId:string, + address?:ModelTypes["AddressComponent"], + loc:ModelTypes["LocComponent"], + formattedAddress:string +}; + ["LocComponent"]: { + type:string, + coordinates:number[] +}; + ["GeolocDistComponent"]: { + calculated?:number, + location?:ModelTypes["LocComponent"] +}; + ["WorkspaceTypeEnum"]: GraphQLTypes["WorkspaceTypeEnum"]; + ["AvailableCurrency"]: GraphQLTypes["AvailableCurrency"]; + ["AvailabilityComponent"]: { + dates:ModelTypes["DateRangeComponent"], + hours?:ModelTypes["HourComponent"][], + exceptions?:ModelTypes["DateExceptionComponent"][], + noWeekend?:boolean +}; + ["DateRangeComponent"]: { + startDate:ModelTypes["DateTime"], + endDate:ModelTypes["DateTime"] +}; + ["HourComponent"]: { + from:string, + to:string +}; + ["DateExceptionComponent"]: { + dates:ModelTypes["DateRangeComponent"], + hours?:ModelTypes["HourComponent"][], + allDay?:boolean +}; + ["MainImageComponent"]: { + main?:ModelTypes["ImageComponent"], + one?:ModelTypes["ImageComponent"], + two?:ModelTypes["ImageComponent"], + three?:ModelTypes["ImageComponent"], + four?:ModelTypes["ImageComponent"] +}; + ["BookingSchema"]: { + durationType:ModelTypes["DurationTypeEnum"], + startToEnd?:ModelTypes["DateRangeComponent"], + slot?:ModelTypes["SlotComponent"], + capacity:number, + comments?:string, + status:ModelTypes["StatusEnum"], + paths?:ModelTypes["EnginePathComponent"][], + ownerId:string, + dates:ModelTypes["DateRangeComponent"][], + _id:string, + organisationId?:string, + by?:ModelTypes["MetaBy"], + permissions?:ModelTypes["MetaPermissions"], + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + tagsIds?:string[] +}; + ["IEngineSchema"]: ModelTypes["BookingSchema"] | ModelTypes["BookingEngineSchema"] | ModelTypes["OrderEngineSchema"]; + ["EnginePathComponent"]: { + ressourceModel:ModelTypes["ModelLoadersEnum"], + ressourceId:string +}; + ["ModelLoadersEnum"]: GraphQLTypes["ModelLoadersEnum"]; + ["MetaBy"]: { + createdBy?:string, + updatedBy?:string, + deletedBy?:string +}; + ["MetaPermissions"]: { + r:string[], + w:string[], + d:string[] +}; + ["DurationTypeEnum"]: GraphQLTypes["DurationTypeEnum"]; + ["SlotComponent"]: { + startDate:ModelTypes["DateTime"], + endDate:ModelTypes["DateTime"], + startTime:string, + endTime:string +}; + ["StatusEnum"]: GraphQLTypes["StatusEnum"]; + ["BookingsRessourceEnum"]: GraphQLTypes["BookingsRessourceEnum"]; + ["RangeType"]: GraphQLTypes["RangeType"]; + ["DateTabType"]: GraphQLTypes["DateTabType"]; + ["DateRangeComponentInput"]: GraphQLTypes["DateRangeComponentInput"]; + ["WorkspaceOwnerComponent"]: { + firstName?:string, + lastName?:string +}; + ["GeolocSearchInput"]: GraphQLTypes["GeolocSearchInput"]; + ["GeolocAddressSearchInput"]: GraphQLTypes["GeolocAddressSearchInput"]; + ["BookingEngineSchema"]: { + durationType:ModelTypes["DurationTypeEnum"], + startToEnd?:ModelTypes["DateRangeComponent"], + slot?:ModelTypes["SlotComponent"], + capacity?:number, + comments?:string, + status:ModelTypes["StatusEnum"], + paths?:ModelTypes["EnginePathComponent"][], + ownerId:string, + dates:ModelTypes["DateRangeComponent"][], + owner:ModelTypes["AccountModel"], + _id:string, + organisationId?:string, + by?:ModelTypes["MetaBy"], + permissions?:ModelTypes["MetaPermissions"], + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + tagsIds?:string[], + workspace?:ModelTypes["WorkspaceModel"], + room?:ModelTypes["RoomModel"] +}; + ["RoomModel"]: { + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + r:string[], + w:string[], + d:string[], + title?:string, + stats:ModelTypes["RoomsStats"], + getAccounts?:ModelTypes["AccountModel"][], + getMessages?:ModelTypes["MessageModel"][], + lastMessage?:ModelTypes["DateTime"] +}; + ["RoomsStats"]: { + unreadCounts:ModelTypes["UnreadCount"][] +}; + ["UnreadCount"]: { + accountId:string, + count:number +}; + ["MessageModel"]: { + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + r:string[], + w:string[], + d:string[], + roomId:string, + notified?:boolean, + message:string, + edited:boolean, + deleted:boolean, + file?:ModelTypes["ImageComponent"], + sentBy:string, + readBy:ModelTypes["MessageReadBy"][] +}; + ["MessageReadBy"]: { + accountId:string, + readAt:ModelTypes["DateTime"] +}; + ["OrderEngineSchema"]: { + lines:ModelTypes["LineItemSchema"][], + localeInfo:ModelTypes["LocaleInfo"], + currency:ModelTypes["CurrencyEnum"], + vatExempt:boolean, + accountId:string, + contactInfo?:ModelTypes["BuyerInfoComponent"], + billingInfo?:ModelTypes["BuyerInfoComponent"], + promoId?:string, + invoiceInfo?:ModelTypes["InvoiceInfo"], + providerOrderItems?:ModelTypes["ProviderSchema"][], + orderName?:ModelTypes["TranslatableComponent"], + orderStatus:ModelTypes["OrderStatusEnum"], + paymentIntent:ModelTypes["PaymentIntentInfo"], + paymentStatus?:ModelTypes["PaymentStatusEnum"], + paymentInfo?:ModelTypes["PaymentInfo"], + subTotalPrice:number, + vatPrice:number, + finalPrice:number, + totalPrice:number, + promo?:ModelTypes["PromoModel"], + _id:string, + organisationId?:string, + paths?:ModelTypes["EnginePathComponent"][], + by?:ModelTypes["MetaBy"], + permissions?:ModelTypes["MetaPermissions"], + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + tagsIds?:string[], + getBookingInput?:ModelTypes["BookingNewInput"] +}; + ["LineItemSchema"]: { + _id:string, + productRessource:ModelTypes["ProductRessourceEnum"], + productId:string, + title:string, + price:number, + salesPrice:number, + quantity:number, + vatClassId:string, + parentId?:string, + addOns?:ModelTypes["AddOnComponent"][], + localeInfo?:ModelTypes["LocaleInfo"], + promoId?:string, + finalPrice?:number, + getProduct?:ModelTypes["ProductUnion"], + getLinePrice:number, + getLineVatPrice:number +}; + ["ProductRessourceEnum"]: GraphQLTypes["ProductRessourceEnum"]; + ["AddOnComponent"]: { + addOnId:string, + quantity:number +}; + ["LocaleInfo"]: { + countryCode?:string, + locale?:ModelTypes["AvailableTranslation"] +}; + ["ProductUnion"]:ModelTypes["WorkspaceModel"]; + ["CurrencyEnum"]: GraphQLTypes["CurrencyEnum"]; + ["BuyerInfoComponent"]: { + firstName:string, + lastName:string, + email:string, + company?:string, + vatNumber?:string, + address:ModelTypes["AddressComponent"] +}; + ["InvoiceInfo"]: { + provider:ModelTypes["InvoicingProvider"], + invoiceId:string, + invoiceNumber:number +}; + ["InvoicingProvider"]: GraphQLTypes["InvoicingProvider"]; + ["ProviderSchema"]: { + _id:string, + organisationId:string, + orderStatus:ModelTypes["OrderBusinessStatusEnum"], + lines:ModelTypes["LineItemSchema"][] +}; + ["OrderBusinessStatusEnum"]: GraphQLTypes["OrderBusinessStatusEnum"]; + ["OrderStatusEnum"]: GraphQLTypes["OrderStatusEnum"]; + ["PaymentIntentInfo"]: { + provider:ModelTypes["PaymentProvider"], + stripePaymentIntentData:ModelTypes["StripePaymentIntentData"] +}; + ["PaymentProvider"]: GraphQLTypes["PaymentProvider"]; + ["StripePaymentIntentData"]: { + id:string, + client_secret?:string, + currency:string, + customer:string, + status:string +}; + ["PaymentStatusEnum"]: GraphQLTypes["PaymentStatusEnum"]; + ["PaymentInfo"]: { + provider:ModelTypes["PaymentProvider"], + transactionId:string +}; + ["PromoModel"]: { + _id:string, + organisationId?:string, + createdBy?:string, + updatedBy?:string, + deletedBy?:string, + createdAt:ModelTypes["DateTime"], + updatedAt:ModelTypes["DateTime"], + r:string[], + w:string[], + d:string[], + code:string, + description?:ModelTypes["TranslatableComponent"], + type:ModelTypes["PromoType"], + value:number, + validity?:ModelTypes["DateTime"], + cummulable:boolean, + usageLimit?:number, + usage:number +}; + ["PromoType"]: GraphQLTypes["PromoType"]; + ["BookingNewInput"]: { + durationType:ModelTypes["DurationTypeEnum"], + startToEnd?:ModelTypes["DateRangeComponent"], + slot?:ModelTypes["SlotComponent"], + capacity?:number, + comments?:string, + ressourceModel:ModelTypes["BookingsRessourceEnum"], + ressourceId:string, + checkoutInfo?:ModelTypes["CheckoutEngineInput"] +}; + ["CheckoutEngineInput"]: { + contactInfo:ModelTypes["BuyerInfoComponent"], + billingInfo:ModelTypes["BuyerInfoComponent"], + token?:string +}; + ["Mutation"]: { + updateMe:ModelTypes["AccountModel"], + updateMeEmail:ModelTypes["AccountModel"], + updateMePassword:ModelTypes["AccountModel"], + resetPassword:ModelTypes["SimpleResult"], + registerGuest:ModelTypes["AccountModel"], + register:ModelTypes["AccountModel"], + accountsBillingInfosAddOne:ModelTypes["BillingAddressComponent"], + accountsBillingInfosEditOne:ModelTypes["BillingAddressComponent"], + accountsBillingInfosDeleteOne:string, + accountsPaymentMethodsAddOne:ModelTypes["PaymentMethodDetail"], + accountsPaymentMethodsMarkasDefault:ModelTypes["PaymentMethodDetail"], + accountsPaymentMethodsDeleteOne:string, + accountsLinkWithStripe:string, + accountsRefreshInfoStripe:ModelTypes["AccountModel"], + accountsFavoritesAddOne:ModelTypes["FavoriteComponent"], + accountsFavoritesRemoveOne:ModelTypes["SuccessResponse"], + workspacesAddOne:ModelTypes["WorkspaceModel"], + workspacesEditOne:ModelTypes["WorkspaceModel"], + workspacesDeleteOne:ModelTypes["WorkspaceModel"], + finaliseCheckoutWithStripe:ModelTypes["OrderEngineSchema"], + removeCurrentOrder:ModelTypes["OrderEngineSchema"], + validateOrder:ModelTypes["OrderEngineSchema"], + roomsCreateOne:ModelTypes["RoomModel"], + roomsAddPeople:ModelTypes["RoomModel"], + roomsRemovePeople:ModelTypes["RoomModel"], + roomsMarkAllMessageAsRead:ModelTypes["RoomModel"], + reserveOneBooking:ModelTypes["OrderEngineSchema"], + roomsAddOneMessage:ModelTypes["MessageModel"], + roomsEditOneMessage:ModelTypes["MessageModel"], + roomsMarkMessageAsRead:ModelTypes["MessageModel"], + roomsDeleteOneMessage:ModelTypes["MessageModel"] +}; + ["EditAccountInput"]: GraphQLTypes["EditAccountInput"]; + ["ImageInput"]: GraphQLTypes["ImageInput"]; + ["AddressInput"]: GraphQLTypes["AddressInput"]; + ["SettingsInput"]: GraphQLTypes["SettingsInput"]; + ["NewEmailInput"]: GraphQLTypes["NewEmailInput"]; + ["NewPasswordInput"]: GraphQLTypes["NewPasswordInput"]; + ["LinkEmailInput"]: GraphQLTypes["LinkEmailInput"]; + ["NewAccountInput"]: GraphQLTypes["NewAccountInput"]; + ["BillingAddressInput"]: GraphQLTypes["BillingAddressInput"]; + ["AddressStrictInput"]: GraphQLTypes["AddressStrictInput"]; + ["PaymentMethodInput"]: GraphQLTypes["PaymentMethodInput"]; + ["AddInput"]: GraphQLTypes["AddInput"]; + ["SuccessResponse"]: { + success:boolean +}; + ["RemoveInput"]: GraphQLTypes["RemoveInput"]; + ["NewWorkspaceInput"]: GraphQLTypes["NewWorkspaceInput"]; + ["SEOSimpleInput"]: GraphQLTypes["SEOSimpleInput"]; + ["AvailabilityInput"]: GraphQLTypes["AvailabilityInput"]; + ["HoursInput"]: GraphQLTypes["HoursInput"]; + ["DateExceptionComponentInput"]: GraphQLTypes["DateExceptionComponentInput"]; + ["MainImage"]: GraphQLTypes["MainImage"]; + ["EditWorkspaceInput"]: GraphQLTypes["EditWorkspaceInput"]; + ["CheckoutInput"]: GraphQLTypes["CheckoutInput"]; + ["BuyerInfo"]: GraphQLTypes["BuyerInfo"]; + ["NewRoomInput"]: GraphQLTypes["NewRoomInput"]; + ["AddPeopleToRoomInput"]: GraphQLTypes["AddPeopleToRoomInput"]; + ["RemovePeopleFromRoomInput"]: GraphQLTypes["RemovePeopleFromRoomInput"]; + ["BookingNewInputSchema"]: GraphQLTypes["BookingNewInputSchema"]; + ["SlotComponentInput"]: GraphQLTypes["SlotComponentInput"]; + ["CheckoutEngineInputI"]: GraphQLTypes["CheckoutEngineInputI"]; + ["NewMessageInput"]: GraphQLTypes["NewMessageInput"]; + ["EditMessageInput"]: GraphQLTypes["EditMessageInput"] + } + +export type GraphQLTypes = { + ["Query"]: { + __typename: "Query", + me: GraphQLTypes["AccountModel"], + login: GraphQLTypes["FirebaseTokenResult"], + magicLink: GraphQLTypes["SimpleResult"], + placesAutocomplete: Array, + placesGeocode: GraphQLTypes["JSONObject"], + placesGeocodeFromAddress: GraphQLTypes["JSONObject"], + categoriesGetOne: GraphQLTypes["CategoryModel"], + categoriesGetMany: Array, + categoriesGetCount: number, + listsGetOne: GraphQLTypes["ListModel"], + listsGetMany: Array, + listsGetCount: number, + accountsDashboardLinkStripe: string, + workspacesGetOne: GraphQLTypes["WorkspaceModel"], + workspacesGetMany: Array, + workspacesGetCount: number, + workspacesSearchMany: Array, + workspacesGetMine: Array, + workspacesGetMineCount: number, + bookingsGetOne: GraphQLTypes["BookingEngineSchema"], + bookingsGetMany: Array, + bookingsGetCount: number, + myCurrentOrder: GraphQLTypes["OrderEngineSchema"], + myOrdersGetOne: GraphQLTypes["OrderEngineSchema"], + myOrdersGetMany: Array, + myOrdersGetManyCount: number, + roomsGetOne: GraphQLTypes["RoomModel"], + roomsGetMany: Array, + roomsGetMessages: Array +}; + ["AccountModel"]: { + __typename: "AccountModel", + _id: string, + organisationId: string | null, + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + r: Array, + w: Array, + d: Array, + types: Array, + organisationIds: Array | null, + email: string, + userName: string | null, + firstName: string | null, + lastName: string | null, + profilePicture: GraphQLTypes["ImageComponent"] | null, + phoneNumber: string | null, + gender: GraphQLTypes["AccountGenderEnum"] | null, + birthDate: GraphQLTypes["DateTime"] | null, + getAge: number | null, + address: GraphQLTypes["AddressComponent"] | null, + language: GraphQLTypes["AvailableTranslation"] | null, + settings: Array | null, + securityCheck: boolean, + paymentInfo: GraphQLTypes["AccountPaymentInfo"] | null, + payoutInfo: GraphQLTypes["AccountPayoutInfo"] | null, + terms: boolean, + favLikes: Array | null, + getFavorites: Array +}; + /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */ +["DateTime"]:any; + ["AccountTypeEnum"]: AccountTypeEnum; + ["ImageComponent"]: { + __typename: "ImageComponent", + title: string | null, + fileType: string | null, + large: string, + medium: string | null, + small: string | null +}; + ["AccountGenderEnum"]: AccountGenderEnum; + ["AddressComponent"]: { + __typename: "AddressComponent", + number: string | null, + street: string | null, + streetBis: string | null, + floor: string | null, + box: string | null, + zip: string | null, + state: string | null, + city: string, + country: string +}; + ["AvailableTranslation"]: AvailableTranslation; + ["SettingsComponent"]: { + __typename: "SettingsComponent", + type: GraphQLTypes["SettingsTypeEnum"], + email: boolean | null, + pushNotifications: boolean | null, + sms: boolean | null +}; + ["SettingsTypeEnum"]: SettingsTypeEnum; + ["AccountPaymentInfo"]: { + __typename: "AccountPaymentInfo", + stripeInfo: GraphQLTypes["StripeInfo"] | null, + paymentMethods: Array | null, + billingInfos: Array | null +}; + ["StripeInfo"]: { + __typename: "StripeInfo", + customerId: string +}; + ["PaymentMethodDetail"]: { + __typename: "PaymentMethodDetail", + _id: string, + sPayMethodId: string, + default: boolean, + nameOnCard: string, + type: GraphQLTypes["cardTypeEnum"], + cardInfo: GraphQLTypes["CardInfo"] | null +}; + ["cardTypeEnum"]: cardTypeEnum; + ["CardInfo"]: { + __typename: "CardInfo", + brand: GraphQLTypes["cardBrandEnum"], + country: string, + exp_month: number, + exp_year: number, + last4: string +}; + ["cardBrandEnum"]: cardBrandEnum; + ["BillingAddressComponent"]: { + __typename: "BillingAddressComponent", + firstName: string, + lastName: string, + address: GraphQLTypes["AddressStrictComponent"], + email: string | null, + default: boolean | null, + customTags: GraphQLTypes["JSONObject"] | null, + company: string | null, + vatNumber: string | null, + fiscalForm: string | null, + _id: string +}; + ["AddressStrictComponent"]: { + __typename: "AddressStrictComponent", + number: string, + street: string, + zip: string, + city: string, + country: string, + streetBis: string | null, + floor: string | null, + box: string | null, + state: string | null +}; + /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ +["JSONObject"]:any; + ["AccountPayoutInfo"]: { + __typename: "AccountPayoutInfo", + stripeInfo: GraphQLTypes["StripePayoutInfo"] | null +}; + ["StripePayoutInfo"]: { + __typename: "StripePayoutInfo", + accountId: string, + chargesEnabled: boolean, + payoutsEnabled: boolean, + detailsSubmitted: boolean +}; + ["FavoriteComponent"]: { + __typename: "FavoriteComponent", + ressourceId: string, + ressourceType: GraphQLTypes["RessourceEnum"], + addedAt: GraphQLTypes["DateTime"], + favorite: boolean | null, + liked: boolean | null +}; + ["RessourceEnum"]: RessourceEnum; + ["FirebaseTokenResult"]: { + __typename: "FirebaseTokenResult", + localId: string, + email: string | null, + displayName: string | null, + idToken: string, + registered: boolean | null, + refreshToken: string, + expiresIn: string +}; + ["LoginInput"]: { + email: string, + password: string +}; + ["SimpleResult"]: { + __typename: "SimpleResult", + message: string +}; + ["ResetPasswordInput"]: { + email: string +}; + ["CountryCodesComponentEnum"]: CountryCodesComponentEnum; + ["CategoryModel"]: { + __typename: "CategoryModel", + _id: string, + organisationId: string | null, + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + r: Array, + w: Array, + d: Array, + title: GraphQLTypes["TranslatableComponent"], + teaser: GraphQLTypes["TranslatableComponent"] | null, + cover: GraphQLTypes["ImageComponent"] | null, + thumbnail: GraphQLTypes["ImageComponent"] | null, + extraImages: Array | null, + content: GraphQLTypes["TranslatableComponent"] | null, + seo: GraphQLTypes["SEOField"] | null, + urls: GraphQLTypes["TranslatableComponent"] | null, + ressourceType: GraphQLTypes["RessourceEnum"], + colorCode: string | null +}; + ["TranslatableComponent"]: { + __typename: "TranslatableComponent", + en: string | null, + fr: string, + nl: string | null, + de: string | null +}; + ["SEOField"]: { + __typename: "SEOField", + title: GraphQLTypes["TranslatableComponent"], + description: GraphQLTypes["TranslatableComponent"], + keywords: GraphQLTypes["TranslatableComponent"] | null, + thumbnail: GraphQLTypes["ImageComponent"] | null +}; + ["GetArgs"]: { + limit: number, + skip: number, + sort: string | null +}; + ["ListModel"]: { + __typename: "ListModel", + _id: string, + organisationId: string | null, + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + r: Array, + w: Array, + d: Array, + title: GraphQLTypes["TranslatableComponent"], + teaser: GraphQLTypes["TranslatableComponent"] | null, + cover: GraphQLTypes["ImageComponent"] | null, + thumbnail: GraphQLTypes["ImageComponent"] | null, + extraImages: Array | null, + content: GraphQLTypes["TranslatableComponent"] | null, + seo: GraphQLTypes["SEOField"] | null, + urls: GraphQLTypes["TranslatableComponent"] | null, + ressourceType: GraphQLTypes["ListEnum"], + colorCode: string | null +}; + ["ListEnum"]: ListEnum; + ["WorkspaceModel"]: { + __typename: "WorkspaceModel", + _id: string, + organisationId: string | null, + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + r: Array, + w: Array, + d: Array, + title: string | null, + teaser: string | null, + cover: GraphQLTypes["ImageComponent"] | null, + thumbnail: GraphQLTypes["ImageComponent"] | null, + extraImages: Array | null, + content: string | null, + seo: GraphQLTypes["SEOSimpleField"] | null, + urls: string | null, + place: GraphQLTypes["PlaceComponent"], + dist: GraphQLTypes["GeolocDistComponent"] | null, + getDistFromLocation: number | null, + workspaceType: GraphQLTypes["WorkspaceTypeEnum"], + pricingPerHour: number, + currency: GraphQLTypes["AvailableCurrency"], + maxCapacity: number, + minStay: number, + maxStay: number | null, + availability: GraphQLTypes["AvailabilityComponent"], + mainImages: GraphQLTypes["MainImageComponent"], + equipmentIds: Array, + featureIds: Array, + address: GraphQLTypes["AddressComponent"], + finalPrice: number, + getEquipments: Array | null, + getFeatures: Array | null, + getBookings: Array | null, + getOwner: GraphQLTypes["WorkspaceOwnerComponent"] | null +}; + ["SEOSimpleField"]: { + __typename: "SEOSimpleField", + title: string, + description: string, + keywords: string | null, + thumbnail: GraphQLTypes["ImageComponent"] | null +}; + ["PlaceComponent"]: { + __typename: "PlaceComponent", + placeId: string, + address: GraphQLTypes["AddressComponent"] | null, + loc: GraphQLTypes["LocComponent"], + formattedAddress: string +}; + ["LocComponent"]: { + __typename: "LocComponent", + type: string, + coordinates: Array +}; + ["GeolocDistComponent"]: { + __typename: "GeolocDistComponent", + calculated: number | null, + location: GraphQLTypes["LocComponent"] | null +}; + ["WorkspaceTypeEnum"]: WorkspaceTypeEnum; + ["AvailableCurrency"]: AvailableCurrency; + ["AvailabilityComponent"]: { + __typename: "AvailabilityComponent", + dates: GraphQLTypes["DateRangeComponent"], + hours: Array | null, + exceptions: Array | null, + noWeekend: boolean | null +}; + ["DateRangeComponent"]: { + __typename: "DateRangeComponent", + startDate: GraphQLTypes["DateTime"], + endDate: GraphQLTypes["DateTime"] +}; + ["HourComponent"]: { + __typename: "HourComponent", + from: string, + to: string +}; + ["DateExceptionComponent"]: { + __typename: "DateExceptionComponent", + dates: GraphQLTypes["DateRangeComponent"], + hours: Array | null, + allDay: boolean | null +}; + ["MainImageComponent"]: { + __typename: "MainImageComponent", + main: GraphQLTypes["ImageComponent"] | null, + one: GraphQLTypes["ImageComponent"] | null, + two: GraphQLTypes["ImageComponent"] | null, + three: GraphQLTypes["ImageComponent"] | null, + four: GraphQLTypes["ImageComponent"] | null +}; + ["BookingSchema"]: { + __typename: "BookingSchema", + durationType: GraphQLTypes["DurationTypeEnum"], + startToEnd: GraphQLTypes["DateRangeComponent"] | null, + slot: GraphQLTypes["SlotComponent"] | null, + capacity: number, + comments: string | null, + status: GraphQLTypes["StatusEnum"], + paths: Array | null, + ownerId: string, + dates: Array, + _id: string, + organisationId: string | null, + by: GraphQLTypes["MetaBy"] | null, + permissions: GraphQLTypes["MetaPermissions"] | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + tagsIds: Array | null +}; + ["IEngineSchema"]: { + __typename:"BookingSchema" | "BookingEngineSchema" | "OrderEngineSchema" + _id: string, + organisationId: string | null, + paths: Array | null, + by: GraphQLTypes["MetaBy"] | null, + permissions: GraphQLTypes["MetaPermissions"] | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + tagsIds: Array | null + ['...on BookingSchema']: '__union' & GraphQLTypes["BookingSchema"]; + ['...on BookingEngineSchema']: '__union' & GraphQLTypes["BookingEngineSchema"]; + ['...on OrderEngineSchema']: '__union' & GraphQLTypes["OrderEngineSchema"]; +}; + ["EnginePathComponent"]: { + __typename: "EnginePathComponent", + ressourceModel: GraphQLTypes["ModelLoadersEnum"], + ressourceId: string +}; + ["ModelLoadersEnum"]: ModelLoadersEnum; + ["MetaBy"]: { + __typename: "MetaBy", + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null +}; + ["MetaPermissions"]: { + __typename: "MetaPermissions", + r: Array, + w: Array, + d: Array +}; + ["DurationTypeEnum"]: DurationTypeEnum; + ["SlotComponent"]: { + __typename: "SlotComponent", + startDate: GraphQLTypes["DateTime"], + endDate: GraphQLTypes["DateTime"], + startTime: string, + endTime: string +}; + ["StatusEnum"]: StatusEnum; + ["BookingsRessourceEnum"]: BookingsRessourceEnum; + ["RangeType"]: RangeType; + ["DateTabType"]: DateTabType; + ["DateRangeComponentInput"]: { + startDate: GraphQLTypes["DateTime"], + endDate: GraphQLTypes["DateTime"] +}; + ["WorkspaceOwnerComponent"]: { + __typename: "WorkspaceOwnerComponent", + firstName: string | null, + lastName: string | null +}; + ["GeolocSearchInput"]: { + longitude: number, + latitude: number, + radius: number | null +}; + ["GeolocAddressSearchInput"]: { + formattedAddress: string, + radius: number | null +}; + ["BookingEngineSchema"]: { + __typename: "BookingEngineSchema", + durationType: GraphQLTypes["DurationTypeEnum"], + startToEnd: GraphQLTypes["DateRangeComponent"] | null, + slot: GraphQLTypes["SlotComponent"] | null, + capacity: number | null, + comments: string | null, + status: GraphQLTypes["StatusEnum"], + paths: Array | null, + ownerId: string, + dates: Array, + owner: GraphQLTypes["AccountModel"], + _id: string, + organisationId: string | null, + by: GraphQLTypes["MetaBy"] | null, + permissions: GraphQLTypes["MetaPermissions"] | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + tagsIds: Array | null, + workspace: GraphQLTypes["WorkspaceModel"] | null, + room: GraphQLTypes["RoomModel"] | null +}; + ["RoomModel"]: { + __typename: "RoomModel", + _id: string, + organisationId: string | null, + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + r: Array, + w: Array, + d: Array, + title: string | null, + stats: GraphQLTypes["RoomsStats"], + getAccounts: Array | null, + getMessages: Array | null, + lastMessage: GraphQLTypes["DateTime"] | null +}; + ["RoomsStats"]: { + __typename: "RoomsStats", + unreadCounts: Array +}; + ["UnreadCount"]: { + __typename: "UnreadCount", + accountId: string, + count: number +}; + ["MessageModel"]: { + __typename: "MessageModel", + _id: string, + organisationId: string | null, + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + r: Array, + w: Array, + d: Array, + roomId: string, + notified: boolean | null, + message: string, + edited: boolean, + deleted: boolean, + file: GraphQLTypes["ImageComponent"] | null, + sentBy: string, + readBy: Array +}; + ["MessageReadBy"]: { + __typename: "MessageReadBy", + accountId: string, + readAt: GraphQLTypes["DateTime"] +}; + ["OrderEngineSchema"]: { + __typename: "OrderEngineSchema", + lines: Array, + localeInfo: GraphQLTypes["LocaleInfo"], + currency: GraphQLTypes["CurrencyEnum"], + vatExempt: boolean, + accountId: string, + contactInfo: GraphQLTypes["BuyerInfoComponent"] | null, + billingInfo: GraphQLTypes["BuyerInfoComponent"] | null, + promoId: string | null, + invoiceInfo: GraphQLTypes["InvoiceInfo"] | null, + providerOrderItems: Array | null, + orderName: GraphQLTypes["TranslatableComponent"] | null, + orderStatus: GraphQLTypes["OrderStatusEnum"], + paymentIntent: GraphQLTypes["PaymentIntentInfo"], + paymentStatus: GraphQLTypes["PaymentStatusEnum"] | null, + paymentInfo: GraphQLTypes["PaymentInfo"] | null, + subTotalPrice: number, + vatPrice: number, + finalPrice: number, + totalPrice: number, + promo: GraphQLTypes["PromoModel"] | null, + _id: string, + organisationId: string | null, + paths: Array | null, + by: GraphQLTypes["MetaBy"] | null, + permissions: GraphQLTypes["MetaPermissions"] | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + tagsIds: Array | null, + getBookingInput: GraphQLTypes["BookingNewInput"] | null +}; + ["LineItemSchema"]: { + __typename: "LineItemSchema", + _id: string, + productRessource: GraphQLTypes["ProductRessourceEnum"], + productId: string, + title: string, + price: number, + salesPrice: number, + quantity: number, + vatClassId: string, + parentId: string | null, + addOns: Array | null, + localeInfo: GraphQLTypes["LocaleInfo"] | null, + promoId: string | null, + finalPrice: number | null, + getProduct: GraphQLTypes["ProductUnion"] | null, + getLinePrice: number, + getLineVatPrice: number +}; + ["ProductRessourceEnum"]: ProductRessourceEnum; + ["AddOnComponent"]: { + __typename: "AddOnComponent", + addOnId: string, + quantity: number +}; + ["LocaleInfo"]: { + __typename: "LocaleInfo", + countryCode: string | null, + locale: GraphQLTypes["AvailableTranslation"] | null +}; + ["ProductUnion"]:{ + ['...on WorkspaceModel']: '__union' & GraphQLTypes["WorkspaceModel"]; +}; + ["CurrencyEnum"]: CurrencyEnum; + ["BuyerInfoComponent"]: { + __typename: "BuyerInfoComponent", + firstName: string, + lastName: string, + email: string, + company: string | null, + vatNumber: string | null, + address: GraphQLTypes["AddressComponent"] +}; + ["InvoiceInfo"]: { + __typename: "InvoiceInfo", + provider: GraphQLTypes["InvoicingProvider"], + invoiceId: string, + invoiceNumber: number +}; + ["InvoicingProvider"]: InvoicingProvider; + ["ProviderSchema"]: { + __typename: "ProviderSchema", + _id: string, + organisationId: string, + orderStatus: GraphQLTypes["OrderBusinessStatusEnum"], + lines: Array +}; + ["OrderBusinessStatusEnum"]: OrderBusinessStatusEnum; + ["OrderStatusEnum"]: OrderStatusEnum; + ["PaymentIntentInfo"]: { + __typename: "PaymentIntentInfo", + provider: GraphQLTypes["PaymentProvider"], + stripePaymentIntentData: GraphQLTypes["StripePaymentIntentData"] +}; + ["PaymentProvider"]: PaymentProvider; + ["StripePaymentIntentData"]: { + __typename: "StripePaymentIntentData", + id: string, + client_secret: string | null, + currency: string, + customer: string, + status: string +}; + ["PaymentStatusEnum"]: PaymentStatusEnum; + ["PaymentInfo"]: { + __typename: "PaymentInfo", + provider: GraphQLTypes["PaymentProvider"], + transactionId: string +}; + ["PromoModel"]: { + __typename: "PromoModel", + _id: string, + organisationId: string | null, + createdBy: string | null, + updatedBy: string | null, + deletedBy: string | null, + createdAt: GraphQLTypes["DateTime"], + updatedAt: GraphQLTypes["DateTime"], + r: Array, + w: Array, + d: Array, + code: string, + description: GraphQLTypes["TranslatableComponent"] | null, + type: GraphQLTypes["PromoType"], + value: number, + validity: GraphQLTypes["DateTime"] | null, + cummulable: boolean, + usageLimit: number | null, + usage: number +}; + ["PromoType"]: PromoType; + ["BookingNewInput"]: { + __typename: "BookingNewInput", + durationType: GraphQLTypes["DurationTypeEnum"], + startToEnd: GraphQLTypes["DateRangeComponent"] | null, + slot: GraphQLTypes["SlotComponent"] | null, + capacity: number | null, + comments: string | null, + ressourceModel: GraphQLTypes["BookingsRessourceEnum"], + ressourceId: string, + checkoutInfo: GraphQLTypes["CheckoutEngineInput"] | null +}; + ["CheckoutEngineInput"]: { + __typename: "CheckoutEngineInput", + contactInfo: GraphQLTypes["BuyerInfoComponent"], + billingInfo: GraphQLTypes["BuyerInfoComponent"], + token: string | null +}; + ["Mutation"]: { + __typename: "Mutation", + updateMe: GraphQLTypes["AccountModel"], + updateMeEmail: GraphQLTypes["AccountModel"], + updateMePassword: GraphQLTypes["AccountModel"], + resetPassword: GraphQLTypes["SimpleResult"], + registerGuest: GraphQLTypes["AccountModel"], + register: GraphQLTypes["AccountModel"], + accountsBillingInfosAddOne: GraphQLTypes["BillingAddressComponent"], + accountsBillingInfosEditOne: GraphQLTypes["BillingAddressComponent"], + accountsBillingInfosDeleteOne: string, + accountsPaymentMethodsAddOne: GraphQLTypes["PaymentMethodDetail"], + accountsPaymentMethodsMarkasDefault: GraphQLTypes["PaymentMethodDetail"], + accountsPaymentMethodsDeleteOne: string, + accountsLinkWithStripe: string, + accountsRefreshInfoStripe: GraphQLTypes["AccountModel"], + accountsFavoritesAddOne: GraphQLTypes["FavoriteComponent"], + accountsFavoritesRemoveOne: GraphQLTypes["SuccessResponse"], + workspacesAddOne: GraphQLTypes["WorkspaceModel"], + workspacesEditOne: GraphQLTypes["WorkspaceModel"], + workspacesDeleteOne: GraphQLTypes["WorkspaceModel"], + finaliseCheckoutWithStripe: GraphQLTypes["OrderEngineSchema"], + removeCurrentOrder: GraphQLTypes["OrderEngineSchema"], + validateOrder: GraphQLTypes["OrderEngineSchema"], + roomsCreateOne: GraphQLTypes["RoomModel"], + roomsAddPeople: GraphQLTypes["RoomModel"], + roomsRemovePeople: GraphQLTypes["RoomModel"], + roomsMarkAllMessageAsRead: GraphQLTypes["RoomModel"], + reserveOneBooking: GraphQLTypes["OrderEngineSchema"], + roomsAddOneMessage: GraphQLTypes["MessageModel"], + roomsEditOneMessage: GraphQLTypes["MessageModel"], + roomsMarkMessageAsRead: GraphQLTypes["MessageModel"], + roomsDeleteOneMessage: GraphQLTypes["MessageModel"] +}; + ["EditAccountInput"]: { + userName: string | null, + firstName: string | null, + lastName: string | null, + profilePicture: GraphQLTypes["ImageInput"] | null, + phoneNumber: string | null, + gender: GraphQLTypes["AccountGenderEnum"] | null, + birthDate: GraphQLTypes["DateTime"] | null, + address: GraphQLTypes["AddressInput"] | null, + language: GraphQLTypes["AvailableTranslation"] | null, + settings: Array | null +}; + ["ImageInput"]: { + title: string | null, + fileType: string | null, + large: string, + medium: string | null, + small: string | null +}; + ["AddressInput"]: { + number: string | null, + street: string | null, + streetBis: string | null, + floor: string | null, + box: string | null, + zip: string | null, + state: string | null, + city: string, + country: string +}; + ["SettingsInput"]: { + type: GraphQLTypes["SettingsTypeEnum"], + email: boolean | null, + pushNotifications: boolean | null, + sms: boolean | null +}; + ["NewEmailInput"]: { + newEmail: string +}; + ["NewPasswordInput"]: { + oldPassword: string, + newPassword: string, + newPasswordConfirmation: string +}; + ["LinkEmailInput"]: { + email: string, + password: string, + idToken: string +}; + ["NewAccountInput"]: { + email: string, + password: string, + passwordConfirmation: string, + userName: string | null, + firstName: string | null, + lastName: string | null, + profilePicture: GraphQLTypes["ImageInput"] | null, + phoneNumber: string | null, + gender: GraphQLTypes["AccountGenderEnum"] | null, + birthDate: GraphQLTypes["DateTime"] | null, + address: GraphQLTypes["AddressInput"] | null, + language: GraphQLTypes["AvailableTranslation"] | null, + settings: Array | null, + terms: boolean +}; + ["BillingAddressInput"]: { + firstName: string, + lastName: string, + address: GraphQLTypes["AddressStrictInput"], + email: string | null, + default: boolean | null, + customTags: GraphQLTypes["JSONObject"] | null, + company: string | null, + vatNumber: string | null, + fiscalForm: string | null +}; + ["AddressStrictInput"]: { + number: string, + street: string, + zip: string, + city: string, + country: string, + streetBis: string | null, + floor: string | null, + box: string | null, + state: string | null +}; + ["PaymentMethodInput"]: { + sPayMethodId: string, + nameOnCard: string, + default: boolean | null +}; + ["AddInput"]: { + ressourceId: string, + ressourceType: GraphQLTypes["RessourceEnum"] +}; + ["SuccessResponse"]: { + __typename: "SuccessResponse", + success: boolean +}; + ["RemoveInput"]: { + ressourceId: string +}; + ["NewWorkspaceInput"]: { + title: string | null, + teaser: string | null, + cover: GraphQLTypes["ImageInput"] | null, + thumbnail: GraphQLTypes["ImageInput"] | null, + extraImages: Array | null, + content: string | null, + seo: GraphQLTypes["SEOSimpleInput"] | null, + urls: string | null, + workspaceType: GraphQLTypes["WorkspaceTypeEnum"], + pricingPerHour: number, + currency: GraphQLTypes["AvailableCurrency"], + maxCapacity: number, + minStay: number, + maxStay: number | null, + availability: GraphQLTypes["AvailabilityInput"], + mainImages: GraphQLTypes["MainImage"], + equipmentIds: Array, + featureIds: Array, + address: GraphQLTypes["AddressStrictInput"] +}; + ["SEOSimpleInput"]: { + title: string, + description: string, + keywords: string | null, + thumbnail: GraphQLTypes["ImageInput"] | null +}; + ["AvailabilityInput"]: { + dates: GraphQLTypes["DateRangeComponentInput"], + hours: Array | null, + exceptions: Array | null, + noWeekend: boolean | null +}; + ["HoursInput"]: { + from: string, + to: string +}; + ["DateExceptionComponentInput"]: { + dates: GraphQLTypes["DateRangeComponentInput"], + hours: Array | null, + allDay: boolean | null +}; + ["MainImage"]: { + main: GraphQLTypes["ImageInput"] | null, + one: GraphQLTypes["ImageInput"] | null, + two: GraphQLTypes["ImageInput"] | null, + three: GraphQLTypes["ImageInput"] | null, + four: GraphQLTypes["ImageInput"] | null +}; + ["EditWorkspaceInput"]: { + title: string | null, + teaser: string | null, + cover: GraphQLTypes["ImageInput"] | null, + thumbnail: GraphQLTypes["ImageInput"] | null, + extraImages: Array | null, + content: string | null, + seo: GraphQLTypes["SEOSimpleInput"] | null, + urls: string | null, + workspaceType: GraphQLTypes["WorkspaceTypeEnum"] | null, + pricingPerHour: number | null, + currency: GraphQLTypes["AvailableCurrency"], + maxCapacity: number | null, + minStay: number | null, + maxStay: number | null, + availability: GraphQLTypes["AvailabilityInput"] | null, + mainImages: GraphQLTypes["MainImage"], + equipmentIds: Array | null, + featureIds: Array | null, + address: GraphQLTypes["AddressStrictInput"] | null +}; + ["CheckoutInput"]: { + contactInfo: GraphQLTypes["BuyerInfo"], + billingInfo: GraphQLTypes["BuyerInfo"], + token: string | null +}; + ["BuyerInfo"]: { + firstName: string, + lastName: string, + email: string, + company: string | null, + vatNumber: string | null, + address: GraphQLTypes["AddressInput"] +}; + ["NewRoomInput"]: { + accountIds: Array +}; + ["AddPeopleToRoomInput"]: { + roomId: string, + accountIds: Array +}; + ["RemovePeopleFromRoomInput"]: { + roomId: string, + accountIds: Array +}; + ["BookingNewInputSchema"]: { + durationType: GraphQLTypes["DurationTypeEnum"], + startToEnd: GraphQLTypes["DateRangeComponentInput"] | null, + slot: GraphQLTypes["SlotComponentInput"] | null, + capacity: number | null, + comments: string | null, + ressourceModel: GraphQLTypes["BookingsRessourceEnum"], + ressourceId: string, + checkoutInfo: GraphQLTypes["CheckoutEngineInputI"] | null +}; + ["SlotComponentInput"]: { + startDate: GraphQLTypes["DateTime"], + endDate: GraphQLTypes["DateTime"], + startTime: string, + endTime: string +}; + ["CheckoutEngineInputI"]: { + contactInfo: GraphQLTypes["BuyerInfo"], + billingInfo: GraphQLTypes["BuyerInfo"], + token: string | null +}; + ["NewMessageInput"]: { + roomId: string, + message: string, + file: GraphQLTypes["ImageInput"] | null +}; + ["EditMessageInput"]: { + message: string +} + } +export enum AccountTypeEnum { + admin = "admin", + user = "user", + pro = "pro", + public = "public", + organisationOwner = "organisationOwner", + storeManager = "storeManager" +} +export enum AccountGenderEnum { + m = "m", + f = "f", + other = "other" +} +export enum AvailableTranslation { + en = "en", + fr = "fr", + nl = "nl", + de = "de" +} +export enum SettingsTypeEnum { + notifications = "notifications", + promotions = "promotions", + reminders = "reminders" +} +export enum cardTypeEnum { + alipay = "alipay", + au_becs_debit = "au_becs_debit", + bacs_debit = "bacs_debit", + bancontact = "bancontact", + card = "card", + eps = "eps", + fpx = "fpx", + giropay = "giropay", + ideal = "ideal", + p24 = "p24", + sepa_debit = "sepa_debit", + sofort = "sofort" +} +export enum cardBrandEnum { + amex = "amex", + diners = "diners", + discover = "discover", + jcb = "jcb", + mastercard = "mastercard", + unionpay = "unionpay", + visa = "visa", + unknown = "unknown" +} +export enum RessourceEnum { + workspaces = "workspaces" +} +export enum CountryCodesComponentEnum { + AF = "AF", + AX = "AX", + AL = "AL", + DZ = "DZ", + AS = "AS", + AD = "AD", + AO = "AO", + AI = "AI", + AQ = "AQ", + AG = "AG", + AR = "AR", + AM = "AM", + AW = "AW", + AU = "AU", + AT = "AT", + AZ = "AZ", + BS = "BS", + BH = "BH", + BD = "BD", + BB = "BB", + BY = "BY", + BE = "BE", + BZ = "BZ", + BJ = "BJ", + BM = "BM", + BT = "BT", + BO = "BO", + BA = "BA", + BW = "BW", + BV = "BV", + BR = "BR", + IO = "IO", + BN = "BN", + BG = "BG", + BF = "BF", + BI = "BI", + KH = "KH", + CM = "CM", + CA = "CA", + CV = "CV", + KY = "KY", + CF = "CF", + TD = "TD", + CL = "CL", + CN = "CN", + CX = "CX", + CC = "CC", + CO = "CO", + KM = "KM", + CG = "CG", + CD = "CD", + CK = "CK", + CR = "CR", + CI = "CI", + HR = "HR", + CU = "CU", + CY = "CY", + CZ = "CZ", + DK = "DK", + DJ = "DJ", + DM = "DM", + DO = "DO", + EC = "EC", + EG = "EG", + SV = "SV", + GQ = "GQ", + ER = "ER", + EE = "EE", + ET = "ET", + FK = "FK", + FO = "FO", + FJ = "FJ", + FI = "FI", + FR = "FR", + GF = "GF", + PF = "PF", + TF = "TF", + GA = "GA", + GM = "GM", + GE = "GE", + DE = "DE", + GH = "GH", + GI = "GI", + GR = "GR", + GL = "GL", + GD = "GD", + GP = "GP", + GU = "GU", + GT = "GT", + GG = "GG", + GN = "GN", + GW = "GW", + GY = "GY", + HT = "HT", + HM = "HM", + VA = "VA", + HN = "HN", + HK = "HK", + HU = "HU", + IS = "IS", + IN = "IN", + ID = "ID", + IR = "IR", + IQ = "IQ", + IE = "IE", + IM = "IM", + IL = "IL", + IT = "IT", + JM = "JM", + JP = "JP", + JE = "JE", + JO = "JO", + KZ = "KZ", + KE = "KE", + KI = "KI", + KR = "KR", + KW = "KW", + KG = "KG", + LA = "LA", + LV = "LV", + LB = "LB", + LS = "LS", + LR = "LR", + LY = "LY", + LI = "LI", + LT = "LT", + LU = "LU", + MO = "MO", + MK = "MK", + MG = "MG", + MW = "MW", + MY = "MY", + MV = "MV", + ML = "ML", + MT = "MT", + MH = "MH", + MQ = "MQ", + MR = "MR", + MU = "MU", + YT = "YT", + MX = "MX", + FM = "FM", + MD = "MD", + MC = "MC", + MN = "MN", + ME = "ME", + MS = "MS", + MA = "MA", + MZ = "MZ", + MM = "MM", + NA = "NA", + NR = "NR", + NP = "NP", + NL = "NL", + AN = "AN", + NC = "NC", + NZ = "NZ", + NI = "NI", + NE = "NE", + NG = "NG", + NU = "NU", + NF = "NF", + MP = "MP", + NO = "NO", + OM = "OM", + PK = "PK", + PW = "PW", + PS = "PS", + PA = "PA", + PG = "PG", + PY = "PY", + PE = "PE", + PH = "PH", + PN = "PN", + PL = "PL", + PT = "PT", + PR = "PR", + QA = "QA", + RE = "RE", + RO = "RO", + RU = "RU", + RW = "RW", + BL = "BL", + SH = "SH", + KN = "KN", + LC = "LC", + MF = "MF", + PM = "PM", + VC = "VC", + WS = "WS", + SM = "SM", + ST = "ST", + SA = "SA", + SN = "SN", + RS = "RS", + SC = "SC", + SL = "SL", + SG = "SG", + SK = "SK", + SI = "SI", + SB = "SB", + SO = "SO", + ZA = "ZA", + GS = "GS", + ES = "ES", + LK = "LK", + SD = "SD", + SR = "SR", + SJ = "SJ", + SZ = "SZ", + SE = "SE", + CH = "CH", + SY = "SY", + TW = "TW", + TJ = "TJ", + TZ = "TZ", + TH = "TH", + TL = "TL", + TG = "TG", + TK = "TK", + TO = "TO", + TT = "TT", + TN = "TN", + TR = "TR", + TM = "TM", + TC = "TC", + TV = "TV", + UG = "UG", + UA = "UA", + AE = "AE", + GB = "GB", + US = "US", + UM = "UM", + UY = "UY", + UZ = "UZ", + VU = "VU", + VE = "VE", + VN = "VN", + VG = "VG", + VI = "VI", + WF = "WF", + EH = "EH", + YE = "YE", + ZM = "ZM", + ZW = "ZW", + XK = "XK", + KP = "KP", + SS = "SS", + SX = "SX", + BQ = "BQ", + CW = "CW" +} +export enum ListEnum { + equipment = "equipment", + feature = "feature" +} +export enum WorkspaceTypeEnum { + meeting = "meeting", + individual = "individual", + open = "open" +} +export enum AvailableCurrency { + eur = "eur", + usd = "usd" +} +export enum ModelLoadersEnum { + streams = "streams", + accounts = "accounts", + workspaces = "workspaces", + bookings = "bookings" +} +export enum DurationTypeEnum { + startToEnd = "startToEnd", + slot = "slot" +} +export enum StatusEnum { + draft = "draft", + processing = "processing", + validated = "validated", + cancelled = "cancelled", + expired = "expired" +} +export enum BookingsRessourceEnum { + workspaces = "workspaces" +} +export enum RangeType { + strict = "strict", + intersect = "intersect", + intersectLarge = "intersectLarge", + included = "included" +} +export enum DateTabType { + today = "today", + tomorrow = "tomorrow", + upcomming = "upcomming", + past = "past" +} +export enum ProductRessourceEnum { + workspaces = "workspaces" +} +export enum CurrencyEnum { + eur = "eur", + usd = "usd" +} +export enum InvoicingProvider { + directInvoice = "directInvoice" +} +export enum OrderBusinessStatusEnum { + processing = "processing", + shipped = "shipped", + completed = "completed", + canceled = "canceled", + refund = "refund" +} +export enum OrderStatusEnum { + draft = "draft", + processing = "processing", + shipped = "shipped", + completed = "completed", + canceled = "canceled", + refund = "refund" +} +export enum PaymentProvider { + free = "free", + stripe = "stripe", + bancontact = "bancontact", + bankWire = "bankWire" +} +export enum PaymentStatusEnum { + pending = "pending", + actionNeeded = "actionNeeded", + paid = "paid", + free = "free" +} +export enum PromoType { + fixed = "fixed", + percentage = "percentage" +} +export class GraphQLError extends Error { + constructor(public response: GraphQLResponse) { + super(""); + console.error(response); + } + toString() { + return "GraphQL Response Error"; + } + } + + +export type UnwrapPromise = T extends Promise ? R : T; +export type ZeusState Promise> = NonNullable< + UnwrapPromise> +>; +export type ZeusHook< + T extends ( + ...args: any[] + ) => Record Promise>, + N extends keyof ReturnType +> = ZeusState[N]>; + +type WithTypeNameValue = T & { + __typename?: true; +}; +type AliasType = WithTypeNameValue & { + __alias?: Record>; +}; +export interface GraphQLResponse { + data?: Record; + errors?: Array<{ + message: string; + }>; +} +type DeepAnify = { + [P in keyof T]?: any; +}; +type IsPayLoad = T extends [any, infer PayLoad] ? PayLoad : T; +type IsArray = T extends Array ? InputType[] : InputType; +type FlattenArray = T extends Array ? R : T; + +type NotUnionTypes, DST> = { + [P in keyof DST]: SRC[P] extends '__union' & infer R ? never : P; +}[keyof DST]; + +type ExtractUnions, DST> = { + [P in keyof SRC]: SRC[P] extends '__union' & infer R + ? P extends keyof DST + ? IsArray + : {} + : never; +}[keyof SRC]; + +type IsInterfaced, DST> = FlattenArray extends ZEUS_INTERFACES | ZEUS_UNIONS + ? ExtractUnions & + { + [P in keyof Omit>, '__typename'>]: DST[P] extends true + ? SRC[P] + : IsArray; + } + : { + [P in keyof Pick]: DST[P] extends true ? SRC[P] : IsArray; + }; + + + +export type MapType = SRC extends DeepAnify ? IsInterfaced : never; +type InputType = IsPayLoad extends { __alias: infer R } + ? { + [P in keyof R]: MapType; + } & + MapType, '__alias'>> + : MapType>; +type Func

= (...args: P) => R; +type AnyFunc = Func; +export type ArgsType = F extends Func ? P : never; +export type OperationToGraphQL = (o: Z | V, variables?: Record) => Promise>; +export type SubscriptionToGraphQL = ( + o: Z | V, + variables?: Record, +) => { + ws: WebSocket; + on: (fn: (args: InputType) => void) => void; + off: (e: { data?: InputType; code?: number; reason?: string; message?: string }) => void; + error: (e: { data?: InputType; message?: string }) => void; + open: () => void; +}; +export type CastToGraphQL = (resultOfYourQuery: any) => (o: Z | V) => InputType; +export type SelectionFunction = (t: T | V) => T; +export type fetchOptions = ArgsType; +type websocketOptions = typeof WebSocket extends new ( + ...args: infer R +) => WebSocket + ? R + : never; +export type chainOptions = + | [fetchOptions[0], fetchOptions[1] & {websocket?: websocketOptions}] + | [fetchOptions[0]]; +export type FetchFunction = ( + query: string, + variables?: Record, +) => Promise; +export type SubscriptionFunction = ( + query: string, + variables?: Record, +) => void; +type NotUndefined = T extends undefined ? never : T; +export type ResolverType = NotUndefined; + + + +export const ZeusSelect = () => ((t: any) => t) as SelectionFunction; + +export const ScalarResolver = (scalar: string, value: any) => { + switch (scalar) { + case 'String': + return `${JSON.stringify(value)}`; + case 'Int': + return `${value}`; + case 'Float': + return `${value}`; + case 'Boolean': + return `${value}`; + case 'ID': + return `"${value}"`; + case 'enum': + return `${value}`; + case 'scalar': + return `${value}`; + default: + return false; + } +}; + + +export const TypesPropsResolver = ({ + value, + type, + name, + key, + blockArrays +}: { + value: any; + type: string; + name: string; + key?: string; + blockArrays?: boolean; +}): string => { + if (value === null) { + return `null`; + } + let resolvedValue = AllTypesProps[type][name]; + if (key) { + resolvedValue = resolvedValue[key]; + } + if (!resolvedValue) { + throw new Error(`Cannot resolve ${type} ${name}${key ? ` ${key}` : ''}`) + } + const typeResolved = resolvedValue.type; + const isArray = resolvedValue.array; + const isArrayRequired = resolvedValue.arrayRequired; + if (typeof value === 'string' && value.startsWith(`ZEUS_VAR$`)) { + const isRequired = resolvedValue.required ? '!' : ''; + let t = `${typeResolved}`; + if (isArray) { + if (isRequired) { + t = `${t}!`; + } + t = `[${t}]`; + if(isArrayRequired){ + t = `${t}!`; + } + }else{ + if (isRequired) { + t = `${t}!`; + } + } + return `\$${value.split(`ZEUS_VAR$`)[1]}__ZEUS_VAR__${t}`; + } + if (isArray && !blockArrays) { + return `[${value + .map((v: any) => TypesPropsResolver({ value: v, type, name, key, blockArrays: true })) + .join(',')}]`; + } + const reslovedScalar = ScalarResolver(typeResolved, value); + if (!reslovedScalar) { + const resolvedType = AllTypesProps[typeResolved]; + if (typeof resolvedType === 'object') { + const argsKeys = Object.keys(resolvedType); + return `{${argsKeys + .filter((ak) => value[ak] !== undefined) + .map( + (ak) => `${ak}:${TypesPropsResolver({ value: value[ak], type: typeResolved, name: ak })}` + )}}`; + } + return ScalarResolver(AllTypesProps[typeResolved], value) as string; + } + return reslovedScalar; +}; + + +const isArrayFunction = ( + parent: string[], + a: any[] +) => { + const [values, r] = a; + const [mainKey, key, ...keys] = parent; + const keyValues = Object.keys(values).filter((k) => typeof values[k] !== 'undefined'); + + if (!keys.length) { + return keyValues.length > 0 + ? `(${keyValues + .map( + (v) => + `${v}:${TypesPropsResolver({ + value: values[v], + type: mainKey, + name: key, + key: v + })}` + ) + .join(',')})${r ? traverseToSeekArrays(parent, r) : ''}` + : traverseToSeekArrays(parent, r); + } + + const [typeResolverKey] = keys.splice(keys.length - 1, 1); + let valueToResolve = ReturnTypes[mainKey][key]; + for (const k of keys) { + valueToResolve = ReturnTypes[valueToResolve][k]; + } + + const argumentString = + keyValues.length > 0 + ? `(${keyValues + .map( + (v) => + `${v}:${TypesPropsResolver({ + value: values[v], + type: valueToResolve, + name: typeResolverKey, + key: v + })}` + ) + .join(',')})${r ? traverseToSeekArrays(parent, r) : ''}` + : traverseToSeekArrays(parent, r); + return argumentString; +}; + + +const resolveKV = (k: string, v: boolean | string | { [x: string]: boolean | string }) => + typeof v === 'boolean' ? k : typeof v === 'object' ? `${k}{${objectToTree(v)}}` : `${k}${v}`; + + +const objectToTree = (o: { [x: string]: boolean | string }): string => + `{${Object.keys(o).map((k) => `${resolveKV(k, o[k])}`).join(' ')}}`; + + +const traverseToSeekArrays = (parent: string[], a?: any): string => { + if (!a) return ''; + if (Object.keys(a).length === 0) { + return ''; + } + let b: Record = {}; + if (Array.isArray(a)) { + return isArrayFunction([...parent], a); + } else { + if (typeof a === 'object') { + Object.keys(a) + .filter((k) => typeof a[k] !== 'undefined') + .map((k) => { + if (k === '__alias') { + Object.keys(a[k]).map((aliasKey) => { + const aliasOperations = a[k][aliasKey]; + const aliasOperationName = Object.keys(aliasOperations)[0]; + const aliasOperation = aliasOperations[aliasOperationName]; + b[ + `${aliasOperationName}__alias__${aliasKey}: ${aliasOperationName}` + ] = traverseToSeekArrays([...parent, aliasOperationName], aliasOperation); + }); + } else { + b[k] = traverseToSeekArrays([...parent, k], a[k]); + } + }); + } else { + return ''; + } + } + return objectToTree(b); +}; + + +const buildQuery = (type: string, a?: Record) => + traverseToSeekArrays([type], a); + + +const inspectVariables = (query: string) => { + const regex = /\$\b\w*__ZEUS_VAR__\[?[^!^\]^\s^,^\)^\}]*[!]?[\]]?[!]?/g; + let result; + const AllVariables: string[] = []; + while ((result = regex.exec(query))) { + if (AllVariables.includes(result[0])) { + continue; + } + AllVariables.push(result[0]); + } + if (!AllVariables.length) { + return query; + } + let filteredQuery = query; + AllVariables.forEach((variable) => { + while (filteredQuery.includes(variable)) { + filteredQuery = filteredQuery.replace(variable, variable.split('__ZEUS_VAR__')[0]); + } + }); + return `(${AllVariables.map((a) => a.split('__ZEUS_VAR__')) + .map(([variableName, variableType]) => `${variableName}:${variableType}`) + .join(', ')})${filteredQuery}`; +}; + + +export const queryConstruct = (t: 'query' | 'mutation' | 'subscription', tName: string) => (o: Record) => + `${t.toLowerCase()}${inspectVariables(buildQuery(tName, o))}`; + + +const fullChainConstruct = (fn: FetchFunction) => (t: 'query' | 'mutation' | 'subscription', tName: string) => ( + o: Record, + variables?: Record, +) => fn(queryConstruct(t, tName)(o), variables).then((r:any) => { + seekForAliases(r) + return r +}); + +export const fullChainConstructor = ( + fn: F, + operation: 'query' | 'mutation' | 'subscription', + key: R, +) => + ((o, variables) => fullChainConstruct(fn)(operation, key)(o as any, variables)) as OperationToGraphQL< + ValueTypes[R], + GraphQLTypes[R] + >; + + +const fullSubscriptionConstruct = (fn: SubscriptionFunction) => ( + t: 'query' | 'mutation' | 'subscription', + tName: string, +) => (o: Record, variables?: Record) => + fn(queryConstruct(t, tName)(o), variables); + +export const fullSubscriptionConstructor = ( + fn: F, + operation: 'query' | 'mutation' | 'subscription', + key: R, +) => + ((o, variables) => fullSubscriptionConstruct(fn)(operation, key)(o as any, variables)) as SubscriptionToGraphQL< + ValueTypes[R], + GraphQLTypes[R] + >; + + +const seekForAliases = (response: any) => { + const traverseAlias = (value: any) => { + if (Array.isArray(value)) { + value.forEach(seekForAliases); + } else { + if (typeof value === 'object') { + seekForAliases(value); + } + } + }; + if (typeof response === 'object' && response) { + const keys = Object.keys(response); + if (keys.length < 1) { + return; + } + keys.forEach((k) => { + const value = response[k]; + if (k.indexOf('__alias__') !== -1) { + const [operation, alias] = k.split('__alias__'); + response[alias] = { + [operation]: value, + }; + delete response[k]; + } + traverseAlias(value); + }); + } +}; + + +export const $ = (t: TemplateStringsArray): any => `ZEUS_VAR$${t.join('')}`; + + +export const resolverFor = < + T extends keyof ValueTypes, + Z extends keyof ValueTypes[T], + Y extends ( + args: Required[Z] extends [infer Input, any] ? Input : any, + source: any, + ) => Z extends keyof ModelTypes[T] ? ModelTypes[T][Z] | Promise : any +>( + type: T, + field: Z, + fn: Y, +) => fn as (args?: any,source?: any) => any; + + +const handleFetchResponse = ( + response: Parameters['then']>[0], Function>>[0] +): Promise => { + if (!response.ok) { + return new Promise((_, reject) => { + response.text().then(text => { + try { reject(JSON.parse(text)); } + catch (err) { reject(text); } + }).catch(reject); + }); + } + return response.json(); +}; + +export const apiFetch = (options: fetchOptions) => (query: string, variables: Record = {}) => { + let fetchFunction = fetch; + let queryString = query; + let fetchOptions = options[1] || {}; + if (fetchOptions.method && fetchOptions.method === 'GET') { + queryString = encodeURIComponent(query); + return fetchFunction(`${options[0]}?query=${queryString}`, fetchOptions) + .then(handleFetchResponse) + .then((response: GraphQLResponse) => { + if (response.errors) { + throw new GraphQLError(response); + } + return response.data; + }); + } + return fetchFunction(`${options[0]}`, { + body: JSON.stringify({ query: queryString, variables }), + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + ...fetchOptions + }) + .then(handleFetchResponse) + .then((response: GraphQLResponse) => { + if (response.errors) { + throw new GraphQLError(response); + } + return response.data; + }); + }; + + +export const apiSubscription = (options: chainOptions) => ( + query: string, + variables: Record = {}, + ) => { + try { + const queryString = options[0] + '?query=' + encodeURIComponent(query); + const wsString = queryString.replace('http', 'ws'); + const host = (options.length > 1 && options[1]?.websocket?.[0]) || wsString; + const webSocketOptions = options[1]?.websocket || [host]; + const ws = new WebSocket(...webSocketOptions); + return { + ws, + on: (e: (args: any) => void) => { + ws.onmessage = (event:any) => { + if(event.data){ + const parsed = JSON.parse(event.data) + const data = parsed.data + if (data) { + seekForAliases(data); + } + return e(data); + } + }; + }, + off: (e: (args: any) => void) => { + ws.onclose = e; + }, + error: (e: (args: any) => void) => { + ws.onerror = e; + }, + open: (e: () => void) => { + ws.onopen = e; + }, + }; + } catch { + throw new Error('No websockets implemented'); + } + }; + + +export const Thunder = (fn: FetchFunction, subscriptionFn: SubscriptionFunction) => ({ + query: fullChainConstructor(fn,'query', 'Query'), +mutation: fullChainConstructor(fn,'mutation', 'Mutation') +}); + +export const Chain = (...options: chainOptions) => ({ + query: fullChainConstructor(apiFetch(options),'query', 'Query'), +mutation: fullChainConstructor(apiFetch(options),'mutation', 'Mutation') +}); +export const Zeus = { + query: (o:ValueTypes["Query"]) => queryConstruct('query', 'Query')(o), +mutation: (o:ValueTypes["Mutation"]) => queryConstruct('mutation', 'Mutation')(o) +}; +export const Cast = { + query: ((o: any) => (_: any) => o) as CastToGraphQL< + ValueTypes["Query"], + GraphQLTypes["Query"] +>, +mutation: ((o: any) => (_: any) => o) as CastToGraphQL< + ValueTypes["Mutation"], + GraphQLTypes["Mutation"] +> +}; +export const Selectors = { + query: ZeusSelect(), +mutation: ZeusSelect() +}; + + +export const Gql = Chain('http://localhost:4000') \ No newline at end of file diff --git a/__tests/app/index.ts b/__tests/app/index.ts new file mode 100644 index 0000000..2c2ce53 --- /dev/null +++ b/__tests/app/index.ts @@ -0,0 +1,16 @@ +import { finaliseCheckoutWithStripe, reserveOneBooking } from './src/bookings'; +import { getOneWorkspace, searchWorkspace } from './src/workspace'; + +/* + Workspace +*/ + +// searchWorkspace(); +// getOneWorkspace(); + +/* + Bookings +*/ + +reserveOneBooking(); +// finaliseCheckoutWithStripe(); diff --git a/__tests/app/src/bookings.ts b/__tests/app/src/bookings.ts new file mode 100644 index 0000000..c5c8155 --- /dev/null +++ b/__tests/app/src/bookings.ts @@ -0,0 +1,72 @@ +import { thunder } from '../config'; +import { BookingsRessourceEnum, DurationTypeEnum } from '../helpers/graphql-zeus'; + +export const reserveOneBooking = async () => { + const reserveOneBooking = await thunder.mutation({ + reserveOneBooking: [ + { + input: { + durationType: DurationTypeEnum.slot, + slot: { + startDate: '2021-03-18T23:00:00.000Z', + endDate: '2021-03-19T23:00:00.000Z', + startTime: '12:00', + endTime: '14:30', + }, + ressourceId: '6187782d-0445-4c0d-8c31-6c0c48a95312', + ressourceModel: BookingsRessourceEnum.workspaces, + }, + }, + { + _id: true, + 'finalPrice':true, + subTotalPrice:true, + "vatPrice":true, + paymentIntent: { + stripePaymentIntentData: { + client_secret: true, + id: true, + }, + }, + }, + ], + }); +}; + +export const finaliseCheckoutWithStripe = async () => { + return await thunder.mutation({ + finaliseCheckoutWithStripe: [ + { + input: { + billingInfo: { + firstName: 'Jay', + lastName: 'Son', + email: '', + address: { + city: 'BX', + country: 'Belgium', + }, + }, + contactInfo: { + firstName: 'Jay', + lastName: 'Son', + email: '', + address: { + city: 'BX', + country: 'Belgium', + }, + }, + }, + }, + { + _id: true, + paymentIntent: { + stripePaymentIntentData: { + client_secret: true, + id: true, + }, + }, + }, + ], + }); +}; diff --git a/__tests/app/src/workspace.ts b/__tests/app/src/workspace.ts new file mode 100644 index 0000000..0ab9d3f --- /dev/null +++ b/__tests/app/src/workspace.ts @@ -0,0 +1,49 @@ +import { thunder } from '../config'; +import { BookingsRessourceEnum, DurationTypeEnum } from '../helpers/graphql-zeus'; + +export const searchWorkspace = async () => { + const searchWorkspace = await thunder.query({ + workspacesSearchMany: [ + { + dateFrom: '2021-03-18T00:00:00', + dateTo: '2021-03-20T00:00:00', + hoursFrom: '10:00', + hoursTo: '12:00', + }, + { + _id: true, + title: true, + }, + ], + }); + + console.log(searchWorkspace); + return searchWorkspace; +}; + +export const getOneWorkspace = async () => { + return await thunder.query({ + workspacesGetOne: [ + { + id: '6187782d-0445-4c0d-8c31-6c0c48a95312', + }, + { + _id: true, + title: true, + getOwner: { + firstName: true, + lastName: true, + }, + getBookings: [ + {}, + { + dates: { + startDate: true, + endDate: true, + }, + }, + ], + }, + ], + }); +}; diff --git a/__tests__/booking.gql b/__tests__/booking.gql new file mode 100644 index 0000000..725e530 --- /dev/null +++ b/__tests__/booking.gql @@ -0,0 +1,11 @@ +mutation createBooking { + createOneBooking(input:{ressourceModel:workspaces,ressourceId:"718aa94e-f13d-4ee3-9610-c12c98750c08",dates:{startDate:"2020-11-01T23:15:17.044+0000",endDate:"2020-11-02T23:15:17.044+0000"},capacity:10}){_id,ownerId,owner{email}} +} + +query bookingsGetMany { + bookingsGetMany{_id, workspace{_id,title}} +} + +query bookingsGetOne { + bookingsGetOne(id:"2a3c6b3d-e8e5-4a4d-9c10-6e438ac22e10"){_id, workspace{_id,title}} +} \ No newline at end of file diff --git a/__tests__/signin.gql b/__tests__/signin.gql new file mode 100644 index 0000000..22c01b5 --- /dev/null +++ b/__tests__/signin.gql @@ -0,0 +1,7 @@ + + +mutation resetPassword { + resetPassword(input: { email: "sanawar@makeit-studio.com" }) { + message + } +} \ No newline at end of file diff --git a/__tests__/signup.gql b/__tests__/signup.gql new file mode 100644 index 0000000..730158e --- /dev/null +++ b/__tests__/signup.gql @@ -0,0 +1,2 @@ +query me {me{paymentInfo{stripeInfo{customerId},paymentMethods{sPayMethodId,default,nameOnCard},billingInfos{firstName,lastName,vatNumber}}}} +query login {login(creds:{email:"nicolas.a.bernier@gmail.com",password:"aaaaaa"}){localId,idToken}} diff --git a/__tests__/workspace.gql b/__tests__/workspace.gql new file mode 100644 index 0000000..d46aec7 --- /dev/null +++ b/__tests__/workspace.gql @@ -0,0 +1,66 @@ +query workspacesSearchMany { + workspacesSearchMany( + workspaceType: open + priceMin: 5 + priceMax: 15 + dateFrom: "2020-09-25T15:58:40.803+0000" + dateTo: "2020-12-20T15:58:40.803+0000" + hoursFrom: "08:00" + hoursTo: "12:00" + ) { + _id + title { + en + } + } +} + +query workspaceGetOne { + workspacesGetOne(id: "5061682a-11df-48a8-9894-9ebc577c63d5") { + _id + title { + en + } + } +} + +query workspacesGetMine { + workspacesGetMine { + _id + title { + en + } + } +} +mutation workspacesAddOne { + workspacesAddOne( + input: { + place: { + placeId: "ChIJrZYyJf_Rw0cR8I227YjcDEQ" + loc: { type: "Point", coordinates: [4.3879295, 50.6819832] } + formattedAddress: "Avenue de la Paix 36, 1420 Braine-l'Alleud, Belgium" + } + workspaceType: open + pricingPerHour: 25 + currency: eur + maxCapacity: 4 + minStay: 1 + maxStay: 2 + title: { fr: "Le rouge petillant", en: "The Red Velvet" } + availability: { + dateFrom: "2020-09-24T15:58:40.803+0000" + dateTo: "2020-12-24T15:58:40.803+0000" + weekend: true + hours: [{ from: "08:00", to: "20:00" }] + exceptions: [{ dateFrom: "2020-10-24T15:58:40.803+0000", dateTo: "2020-10-31T15:58:40.803+0000" }] + } + equipmentIds: [] + featureIds: [] + } + ) { + _id + title { + en + } + } +} diff --git a/config/config.ts b/config/config.ts new file mode 100644 index 0000000..2186273 --- /dev/null +++ b/config/config.ts @@ -0,0 +1,24 @@ +import { seedErrorConfig } from '@seed/interfaces/components.errors'; +import { engineBookingErrorConfig } from '@services/module-booking/components.errors'; +import { enginePaymentErrorConfig } from '@services/module-payments/components/components.errors'; + +export enum NotificationEnum { + // emailCodeSignin = "emailCodeSignin", + // magicLink = "magicLink", + adminAccountAddOne = "adminAccountAddOne", + resetPassword = "resetPassword", + bookingUserConfirm = "bookingUserConfirm", + bookingProConfirm = "bookingProConfirm", + bookingUserCancel = "bookingUserCancel", + bookingProCancel = "bookingProCancel", + welcomeUser = "welcomeUser", +} + +export const ErrorsConfig = { + ...seedErrorConfig, + ...enginePaymentErrorConfig, + ...engineBookingErrorConfig, + + // Auth & Registration + 4000: 'You already completed your profile', +}; diff --git a/config/domains.yaml b/config/domains.yaml new file mode 100644 index 0000000..758fa1a --- /dev/null +++ b/config/domains.yaml @@ -0,0 +1,19 @@ +## Instal ## +## run : ./node_modules/.bin/sls plugin install --name serverless-domain-manager --config ./lib/seed/serverless-domains.yaml +## run : SLS_DEBUG=* ./node_modules/.bin/sls create_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile + +## SLS_DEBUG=* ./node_modules/.bin/sls delete_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile +domains: + production: api.work-in-flex.com + staging: staging-api.work-in-flex.com + dev: dev-api.work-in-flex.com + dev-v2: dev-v2-api.work-in-flex.com +customDomain: + basePath: '${self:service}' + domainName: ${self:custom.domains.${self:custom.stage}} + stage: '${self:custom.stage}' + createRoute53Record: true + certificateName: work-in-flex.com + certificateArn: arn:aws:acm:us-east-1:555646219416:certificate/07e83105-1554-4e3d-b968-0a34d77911ad + endpointType: 'edge' + securityPolicy: tls_1_2 diff --git a/lib/__cronjobs/serverless-cron.yaml b/lib/__cronjobs/serverless-cron.yaml new file mode 100644 index 0000000..e21f185 --- /dev/null +++ b/lib/__cronjobs/serverless-cron.yaml @@ -0,0 +1,32 @@ +app: ${file(./package.json):name} +service: cronjob +package: + individually: false +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + AWS_compute: ${self:service}-${self:provider.stage}-compute + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' +plugins: + - serverless-webpack +custom: + stage: ${opt:stage, self:provider.stage} + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk + - puppeteer + +functions: + removeDraftBookings: + handler: lib/__cronjobs/handler.removeDraftBookings + timeout: 300 # optional, in seconds, default is 6 + events: + - schedule: rate(1 minute) diff --git a/lib/__hooks/index.ts b/lib/__hooks/index.ts new file mode 100644 index 0000000..0c23f16 --- /dev/null +++ b/lib/__hooks/index.ts @@ -0,0 +1,29 @@ +export class AsyncHooksService { + /* [MODULES - SHOP] */ + + public async afterCheckout(): Promise { + console.log('[NO ACTION] - afterCheckout'); + } + + public async afterOrderCancel(data,ctx): Promise { + console.log('[NO ACTION] - afterCheckout'); + } + + public async afterOrdersMarkOneAsPaid(data,ctx): Promise { + console.log('[NO ACTION] - afterCheckout'); + } + + public async afterOrdersMarkOneAsUnPaid(data,ctx): Promise { + console.log('[NO ACTION] - afterCheckout'); + } + + public async afterOrdersReimbursed(data,ctx): Promise { + console.log('[NO ACTION] - afterOrdersReimbursed'); + } + + /* [MODULES - EVENTS] */ + + public async afterCheckoutEvent(): Promise { + console.log('[NO ACTION] - afterCheckout'); + } +} diff --git a/lib/seed/.gitignore b/lib/seed/.gitignore new file mode 100644 index 0000000..3c49c37 --- /dev/null +++ b/lib/seed/.gitignore @@ -0,0 +1,19 @@ +node_modules +.DS_Store +package-lock.json + +.build +.webpack + +*/**/.env +*/**/postman.json +*/**/schema.d.ts +*/**/*.d.ts + +src/config/.env.json + +.serverless/ + +config/.env.json + +serverless.yaml diff --git a/lib/seed/README.md b/lib/seed/README.md new file mode 100644 index 0000000..b9eaf43 --- /dev/null +++ b/lib/seed/README.md @@ -0,0 +1,73 @@ +# Make-it Seed V5 + +[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com) + +![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) + +### GrapQL carefull + +- Fields resolvers must await the response (cannot return a promise) +- Classical errors + -- Cannot determine GraphQL output type for 'xxx' of 'YOUR_CLASS' class. Does the value used as its TS type or explicit type is decorated with a proper decorator or is it a proper output value? + --- You forgot to put @ObjectType or @InputType in your class definition. Or your forgot to precise on the decorator what type of field it was @Field(() => YOUR_CLASS) + +### Serverless carefull + +- Test your serverless file npx sls print --config ./devOps/serverless-upload.yaml + +### Installation + +In order to make the seed run and setup your project you need to + +- copy the account service in your application folder (see example folder) +- copy the \_\_loaders file in your application folder (see example folder) +- copy the \_\_resolvers.ts file in your application folder (see example folder) + +- copy the .env file in your config folder (see example folder) + - put the correct admin email + +### Domains + +- run : ./node_modules/.bin/sls plugin install --name serverless-domain-manager --config ./lib/seed/serverless-domains.yaml +- run : SLS_DEBUG=\* ./node_modules/.bin/sls create_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile + +- to delete : SLS_DEBUG=\* ./node_modules/.bin/sls delete_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile + +### VSCODE CONFIG + +npm i -g eslint eslint-config-prettier eslint-plugin-prettier prettier + +### If you need REST APIS + +npm install --save express serverless-http + +Check examples/app-rest.ts + +# SOME ENV CONFIG + +TWILIO_FROM_NUMBER +TWILIO_ACCOUNT_SID +TWILIO_AUTH_TOKEN + +# How to write tests + +npm i -g graphql-zeus +Have your dev running +zeus http://localhost:4000 ./**tests/app/helpers --ts +zeus http://localhost:4001 ./**tests/admin/helpers --ts + +# Search Engine + +--- Free form text +(1) Create text index on searchT field + +# Cronjobs + +copy examples/**cronjobs to lib/**cronjobs + +Add this to your package.json +"cronjobs": [ +"./lib/__cronjobs", +"serverless-cron.yaml", +"handler.ts" +] diff --git a/lib/seed/__loaders.ts b/lib/seed/__loaders.ts new file mode 100644 index 0000000..a7c18cb --- /dev/null +++ b/lib/seed/__loaders.ts @@ -0,0 +1,46 @@ +import CategoryModel from '@services/module-cms/functions/categories/category.model'; + +import ListModel from '@services/module-cms/functions/lists/list.model'; +import PageModel from '@services/module-cms/functions/pages/pages.model'; + +import AccountModel from '@src/accounts/account.model'; +import DataLoader from 'dataloader'; +import StreamEngineModel from './engine/utils/streams/stream.model'; +import { buildEngineLoader, buildLoader } from './services/database/LoaderService'; +import NotificationModel from './services/notifications/notifications.model'; + +/* + + !!! Do not use, copy on parent only ! + +*/ +const seedModelLoaders = { + 'stream.changes': new StreamEngineModel(), + streams: new StreamEngineModel(), + notifications: new NotificationModel(), + accounts: new AccountModel(), + pages: new PageModel(), +}; +class SeedLoaders { + /* CMS */ + + listLoader: DataLoader; + categoryLoader: DataLoader; + // tagsLoader: DataLoader; + + // answerLoader: DataLoader; + + /* ACCOUNT */ + + accountLoader: DataLoader; + + public constructor() { + this.listLoader = buildLoader(new ListModel()); + this.categoryLoader = buildLoader(new CategoryModel()); + // this.tagsLoader = buildEngineLoader(new TagModel()); + + // this.answerLoader = buildEngineLoader(new AnswerEngineModel()); + + this.accountLoader = buildLoader(new AccountModel()); + } +} diff --git a/lib/seed/__tests/helper.ts b/lib/seed/__tests/helper.ts new file mode 100644 index 0000000..72e2f08 --- /dev/null +++ b/lib/seed/__tests/helper.ts @@ -0,0 +1,33 @@ +import { clog } from '../helpers/Utils'; +import fetch from 'node-fetch'; + +export function testRunQuery(url: string, token: string): (query: string, variables?: Record) => Promise { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + return async (query, variables) => { + const headers: any = { + 'Content-Type': 'application/json', + }; + + if (token && (token as any) != '') headers.authorization = token; + + const r = JSON.stringify({ query, variables }); + clog({ query, variables }); + + const response = await fetch(url, { + body: r, + method: 'POST', + headers: headers, + }); + + try { + const text = await response.text(); + const res = JSON.parse(text); + if (res.errors) clog(res.errors[0]); + clog(res.data); + } catch (err) { + console.log(err); + } + + return; + }; +} diff --git a/lib/seed/app-admin.ts b/lib/seed/app-admin.ts new file mode 100644 index 0000000..945d036 --- /dev/null +++ b/lib/seed/app-admin.ts @@ -0,0 +1,66 @@ +import 'reflect-metadata'; +import { authMiddleware, errorMiddleware, ctxMiddleware, complexityMiddleware } from '@seed/graphql/Middleware'; + +import Firebase from '@seed/services/auth/FirebaseService'; + +import { AdminResolvers } from '@src/__indexes/__resolvers.index.admin'; +import { SettingsCache } from './graphql/Settings'; +import { buildSchema } from 'type-graphql'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { ApolloServer } = require('apollo-server'); + +const PORT = process.env.PORT || 4001; + +const bootstrap = async (): Promise => { + process.env.NODE_ENV = 'local'; + + const settingsI = SettingsCache.getInstance(); + await settingsI.refreshCache(); + + try { + const schema = await buildSchema({ + resolvers: AdminResolvers as any, + authChecker: authMiddleware, + }); + + const server = new ApolloServer({ + // typeDefs, + // resolvers, + schema, + formatError: errorMiddleware, + formatResponse: (response): any => { + return response; + }, + context: ctxMiddleware, + tracing: false, + plugins: [ + { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + requestDidStart: () => ({ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + didResolveOperation({ request, document }) { + return complexityMiddleware(schema, request.variables, document); + }, + }), + }, + ], + }); + + try { + const token = await Firebase.getInstance().createTokenId(process.env.TEST_ADMIN_EMAIL ? process.env.TEST_ADMIN_EMAIL : ''); + console.log(token); + } catch (error) { + console.log('no token available'); + } + + server.listen(PORT).then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); + }); + } catch (error) { + console.log('[GRAPH ERROR]', error); + throw error; + } +}; + +bootstrap(); diff --git a/lib/seed/app.ts b/lib/seed/app.ts new file mode 100644 index 0000000..311e8da --- /dev/null +++ b/lib/seed/app.ts @@ -0,0 +1,72 @@ +import 'reflect-metadata'; +import { buildSchema, NonEmptyArray } from 'type-graphql'; + +import { authMiddleware, errorMiddleware, ctxMiddleware, complexityMiddleware } from '@seed/graphql/Middleware'; + +import Firebase from '@seed/services/auth/FirebaseService'; + +import { AppResolvers } from '@src/__indexes/__resolvers.index'; +import { SettingsCache } from './graphql/Settings'; +import { runBeforeAll } from '@src/__indexes/__bootstrap'; +import { nullToUndefined } from './helpers/Utils'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { ApolloServer } = require('apollo-server'); + +const PORT = process.env.PORT || 4000; + +const bootstrap = async (): Promise => { + process.env.NODE_ENV = 'local'; + + const settingsI = SettingsCache.getInstance(); + await settingsI.refreshCache(); + + try { + const schema = await buildSchema({ + resolvers: (AppResolvers as unknown) as NonEmptyArray, + authChecker: authMiddleware, + validate: false, // disable automatic validation or pass the default config object + }); + + const server = new ApolloServer({ + // typeDefs, + // resolvers, + schema, + formatError: errorMiddleware, + formatResponse: (response): any => { + return nullToUndefined(response); + }, + context: ctxMiddleware, + tracing: false, + plugins: [ + { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + requestDidStart: () => ({ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + didResolveOperation({ request, document }) { + return complexityMiddleware(schema, request.variables, document); + }, + }), + }, + ], + }); + + try { + const token = await Firebase.getInstance().createTokenId(process.env.TEST_USER_EMAIL ? process.env.TEST_USER_EMAIL : ''); + console.log(`Token for ${process.env.TEST_USER_EMAIL}`, token.idToken); + } catch (error) { + console.log('no token available'); + } + + await runBeforeAll(); + + server.listen(PORT).then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); + }); + } catch (error) { + console.log('[GRAPH ERROR]', error); + throw error; + } +}; + +bootstrap(); diff --git a/lib/seed/devops/deploy-domains.js b/lib/seed/devops/deploy-domains.js new file mode 100644 index 0000000..2b563a5 --- /dev/null +++ b/lib/seed/devops/deploy-domains.js @@ -0,0 +1,117 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const execa = require('execa'); +const Listr = require('listr'); +const package = require('../../../package.json'); + +const fs = require('fs'); + +if (!package.devops) throw `No devops configuration found in package.json`; +if (!package.devops.services) throw `No devops services found in package.json`; +if (!package.devops.awsProfile) throw `No devops awsProfile found in package.json`; +if (!package.devops.awsRegion) throw 'No devops awsRegion found in package.json'; + +// Config input +const awsProfile = package.devops.awsProfile; +const awsRegion = package.devops.awsRegion; + +// Arguments checks + +const deployBase = function() { + return new Listr( + [ + { + title: 'Deploy domains for DEV', + task: async () => { + const subprocess = execa( + 'npx', + [ + './node_modules/.bin/sls', + 'create_domain', + '--config', + './lib/seed/devops/serverless-domains.yaml', + '--stage', + 'dev', + '--aws-profile', + awsProfile, + '--region', + awsRegion, + ], + { env: { 'SLS_DEBUG=*': '*' } }, + ); + + subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' })); + + const { stdout } = await subprocess; + }, + }, + { + title: 'Deploy domains for STAGING', + task: async () => { + const subprocess = execa( + 'npx', + [ + './node_modules/.bin/sls', + 'create_domain', + '--config', + './lib/seed/devops/serverless-domains.yaml', + '--stage', + 'staging', + '--aws-profile', + awsProfile, + '--region', + awsRegion, + ], + { env: { 'SLS_DEBUG=*': '*' } }, + ); + + subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' })); + + const { stdout } = await subprocess; + }, + }, + { + title: 'Deploy domains for PRODUCTION', + task: async () => { + const subprocess = execa( + 'npx', + [ + './node_modules/.bin/sls', + 'create_domain', + '--config', + './lib/seed/devops/serverless-domains.yaml', + '--stage', + 'production', + '--aws-profile', + awsProfile, + '--region', + awsRegion, + ], + { env: { 'SLS_DEBUG=*': '*' } }, + ); + + subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' })); + + const { stdout } = await subprocess; + }, + }, + ], + { concurrent: false }, + ); +}; + +const tasksList = []; + +tasksList.push({ + title: 'Deploying Domains', + task: () => { + return deployBase(); + }, +}); + +const tasks = new Listr(tasksList, { concurrent: false }); + +tasks.run().catch((err) => { + console.error(err); +}); diff --git a/lib/seed/devops/deploy-v2.js b/lib/seed/devops/deploy-v2.js new file mode 100644 index 0000000..63001c2 --- /dev/null +++ b/lib/seed/devops/deploy-v2.js @@ -0,0 +1,98 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const execa = require('execa'); +const Listr = require('listr'); +const package = require('../../../package.json'); + +const fs = require('fs'); + +if (!package.devops) throw `No devops configuration found in package.json`; +if (!package.devops.services) throw `No devops services found in package.json`; +if (!package.devops.awsProfile) throw `No devops awsProfile found in package.json`; +if (!package.devops.awsRegion) throw 'No devops awsRegion found in package.json'; + +// Config input +const paths = package.devops.services; +const awsProfile = package.devops.awsProfile; +const awsRegion = package.devops.awsRegion; + +// Arguments checks + +const stage = process.argv[2] || 'dev'; +const sName = process.argv[3] || 'all'; + +const deployBase = function(basePath) { + const baseDirectory = basePath[0]; + const serverlessFile = baseDirectory + '/' + basePath[1]; + + let handlerFile; + if (basePath[2]) handlerFile = baseDirectory + '/' + basePath[2]; + else handlerFile = baseDirectory + '/' + 'handler.ts'; + + return new Listr( + [ + { + title: 'Remove handler file', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}), + }, + { + title: 'Copy handler file', + task: () => { + console.log('handler file here', handlerFile); + execa('cp', [handlerFile, 'handler.ts']).then((result) => {}); + }, + }, + { + title: 'Deploy serverless file', + task: async () => { + console.log('serverless file here', serverlessFile); + + const subprocess = execa( + 'npx', + ['sls', 'deploy', '--config', serverlessFile, '--stage', stage, '--aws-profile', awsProfile, '--region', awsRegion], + { env: { 'SLS_DEBUG=*': '*' } }, + ); + + console.log('deploying logs here', '.slslogs.txt'); + subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' })); + + const { stdout } = await subprocess; + }, + }, + { + title: 'Remove handler file', + task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}), + }, + ], + { concurrent: false }, + ); +}; + +let functions = Object.keys(paths); + +if (sName != 'all') { + functions = sName.split(','); + for (let index = 0; index < functions.length; index++) { + if (!paths.hasOwnProperty(functions[index])) throw `Bad functions name for ${functions[index]}`; + } +} + +const tasksList = []; + +for (let index = 0; index < functions.length; index++) { + const element = functions[index]; + tasksList.push({ + title: 'Deploying' + ' ' + element, + task: () => { + return deployBase(paths[element]); + }, + }); +} + +const tasks = new Listr(tasksList, { concurrent: false }); + +tasks.run().catch((err) => { + console.error(err); +}); diff --git a/lib/seed/devops/deploy.js b/lib/seed/devops/deploy.js new file mode 100644 index 0000000..b230566 --- /dev/null +++ b/lib/seed/devops/deploy.js @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-var-requires */ +const execa = require('execa'); +const Listr = require('listr'); +const package = require('../../../package.json'); + +const fs = require('fs'); + +if (!package.devops) throw `No devops configuration found in package.json`; +if (!package.devops.services) throw `No devops services found in package.json`; +if (!package.devops.awsProfile) throw `No devops awsProfile found in package.json`; +if (!package.devops.awsRegion) throw 'No devops awsRegion found in package.json'; + +// Config input +const paths = package.devops.services; +const awsProfile = package.devops.awsProfile; +const awsRegion = package.devops.awsRegion; + +// Arguments checks + +const stage = process.argv[2] || 'dev'; +const sName = process.argv[3] || 'all'; + +const deployBase = function(basePath) { + const baseDirectory = basePath[0]; + const serverlessFile = baseDirectory + '/' + basePath[1]; + + let handlerFile; + if (basePath[2]) handlerFile = baseDirectory + '/' + basePath[2]; + else handlerFile = baseDirectory + '/' + 'handler.ts'; + + return new Listr( + [ + { + title: 'Remove handler file', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}), + }, + { + title: 'Copy handler file', + task: () => { + console.log('handler file here', handlerFile); + execa('cp', [handlerFile, 'handler.ts']).then((result) => {}); + }, + }, + { + title: 'Copy serverless file', + task: () => { + console.log('serverless file here', serverlessFile); + execa('cp', [serverlessFile, 'serverless.yaml']).then((result) => {}); + }, + }, + { + title: 'Deploy serverless file', + task: async () => { + const subprocess = execa( + 'npx', + ['sls', 'deploy', /*'--config', serverlessFile,*/ '--stage', stage, '--aws-profile', awsProfile, '--region', awsRegion], + { env: { 'SLS_DEBUG=*': '*' } }, + ); + + console.log('deploying logs here', '.slslogs.txt'); + subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' })); + + const { stdout } = await subprocess; + }, + }, + { + title: 'Remove handler file', + task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}), + }, + ], + { concurrent: false }, + ); +}; + +let functions = Object.keys(paths); + +if (sName != 'all') { + functions = sName.split(','); + for (let index = 0; index < functions.length; index++) { + if (!paths.hasOwnProperty(functions[index])) throw `Bad functions name for ${functions[index]}`; + } +} + +const tasksList = []; + +for (let index = 0; index < functions.length; index++) { + const element = functions[index]; + tasksList.push({ + title: 'Deploying' + ' ' + element, + task: () => { + return deployBase(paths[element]); + }, + }); +} + +const tasks = new Listr(tasksList, { concurrent: false }); + +tasks.run().catch((err) => { + console.error(err); +}); diff --git a/lib/seed/devops/handler-admin.ts b/lib/seed/devops/handler-admin.ts new file mode 100644 index 0000000..b796ab6 --- /dev/null +++ b/lib/seed/devops/handler-admin.ts @@ -0,0 +1,10 @@ +import 'reflect-metadata'; +import { ServerInstance } from '@seed/graphql/Server'; + +import { AdminResolvers } from '@src/__indexes/__resolvers.index.admin'; + +export async function adminHandler(event: any, context: any): Promise { + const apolloInstance = await ServerInstance.getInstance(AdminResolvers); + const response = await apolloInstance.run(event, context); + return response; +} diff --git a/lib/seed/devops/handler-app.ts b/lib/seed/devops/handler-app.ts new file mode 100644 index 0000000..b1feef5 --- /dev/null +++ b/lib/seed/devops/handler-app.ts @@ -0,0 +1,9 @@ +import 'reflect-metadata'; +import { ServerInstance } from '@seed/graphql/Server'; +import { AppResolvers } from '@src/__indexes/__resolvers.index'; + +export async function appHandler(event: any, context: any): Promise { + const apolloInstance = await ServerInstance.getInstance(AppResolvers); + const response = await apolloInstance.run(event, context); + return response; +} diff --git a/lib/seed/devops/handler-hooks.ts b/lib/seed/devops/handler-hooks.ts new file mode 100644 index 0000000..4c174a8 --- /dev/null +++ b/lib/seed/devops/handler-hooks.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import 'reflect-metadata'; + +import { modelsLoaders } from '@src/__indexes/__loaders'; +import ChangeStreamModel from '@lib/seed/services/change-stream/change-stream.model'; +import { StreamOperationType, PostHookStatus } from '@lib/seed/services/change-stream/change-stream.components'; +import { AsyncHooksService } from '@lib/__hooks'; +import { createApolloContext } from '@seed/graphql/Middleware'; +import { SettingsCache } from '@seed/graphql/Settings'; + +export const hookHandler = async (event: any): Promise => { + const settingsI = SettingsCache.getInstance(); + await settingsI.refreshCache(); + + process.env.isHook = 'true'; + + const { streamId, notificationRessourceId } = event; + + if (streamId) { + await streamHandler(streamId); + } else if (notificationRessourceId) { + await notificationHandler(notificationRessourceId); + } else { + console.log('[ASYNC - ERROR]', 'was called with no info'); + } + + return; +}; + +async function notificationHandler(streamId: string) { + const stream = await (await new ChangeStreamModel().db()).findOneAndUpdate( + { _id: streamId, hookStatus: PostHookStatus.new }, + { $set: { hookStatus: PostHookStatus.inProcess } }, + ); + + if (stream && stream.value) { + const streamData = stream.value; + console.log('[ASYNC - STREAM] Stream', stream.value.collection, stream.value.operation); + + try { + if (streamData.operation == StreamOperationType.hook) { + const HookService = new AsyncHooksService(); + + if (!HookService[streamData.collection]) + throw `[ASYNC - STREAM] ERROR - No AsyncHooksService for this ${streamData.collection}. Are you sure you put it in the AsyncHooksService ?`; + + if (streamData.insertedValues && streamData.insertedValues.length > 0) { + const paramsArray = streamData.insertedValues as any[]; + + for (let index = 0; index < paramsArray.length; index++) { + // Deal with CTX + if (paramsArray[index].ctx) { + paramsArray[index] = await createApolloContext(paramsArray[index].ctx.user._id, paramsArray[index].ctx.organisationId); + } + } + await HookService[streamData.collection](...paramsArray); + } else await HookService[streamData.collection](); + } else { + if (!modelsLoaders[streamData.collection]) + throw `[ASYNC - STREAM] ERROR - No ModelLoaders for this ${streamData.collection}. Are you sure you put it in the ModelLoaders ?`; + + console.log('streamData', streamData); + const model = modelsLoaders[streamData.collection]; + const modelData = await model.getOne({ _id: streamData.documentKey }, null); + console.log('model', model); + + switch (streamData.operation) { + case StreamOperationType.insert: + await model.afterCreate(); + break; + case StreamOperationType.update: + await model.afterUpdate(); + break; + case StreamOperationType.delete: + await model.afterDelete(); + break; + default: + throw `[ASYNC - STREAM] ERROR - No operation ${streamData.operation} for this ${streamData.collection}`; + } + } + await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.completed } }); + console.log('[ASYNC - STREAM] COMPLETE', { _id: streamId }); + } catch (error) { + console.error('[ASYNC - STREAM] ERROR', error); + await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.error } }); + } + } else { + console.error('[ASYNC - STREAM] ERROR', 'No new stream found', streamId); + } +} + +async function streamHandler(streamId: string) { + const stream = await (await new ChangeStreamModel().db()).findOneAndUpdate( + { _id: streamId, hookStatus: PostHookStatus.new }, + { $set: { hookStatus: PostHookStatus.inProcess } }, + ); + + if (stream && stream.value) { + const streamData = stream.value; + console.log('[ASYNC - STREAM] Stream', stream.value.collection, stream.value.operation); + + try { + if (streamData.operation == StreamOperationType.hook) { + const HookService = new AsyncHooksService(); + + if (!HookService[streamData.collection]) + throw `[ASYNC - STREAM] ERROR - No AsyncHooksService for this ${streamData.collection}. Are you sure you put it in the AsyncHooksService ?`; + + if (streamData.insertedValues && streamData.insertedValues.length > 0) { + const paramsArray = streamData.insertedValues as any[]; + + for (let index = 0; index < paramsArray.length; index++) { + // Deal with CTX + if (paramsArray[index].ctx) { + paramsArray[index] = await createApolloContext(paramsArray[index].ctx.user._id, paramsArray[index].ctx.organisationId); + } + } + await HookService[streamData.collection](...paramsArray); + } else await HookService[streamData.collection](); + } else { + if (!modelsLoaders[streamData.collection]) + throw `[ASYNC - STREAM] ERROR - No ModelLoaders for this ${streamData.collection}. Are you sure you put it in the ModelLoaders ?`; + + console.log('streamData', streamData); + const model = modelsLoaders[streamData.collection]; + const modelData = await model.getOne({ _id: streamData.documentKey }, null); + console.log('model', model); + + switch (streamData.operation) { + case StreamOperationType.insert: + await model.afterCreate(); + break; + case StreamOperationType.update: + await model.afterUpdate(); + break; + case StreamOperationType.delete: + await model.afterDelete(); + break; + default: + throw `[ASYNC - STREAM] ERROR - No operation ${streamData.operation} for this ${streamData.collection}`; + } + } + await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.completed } }); + console.error('[ASYNC - STREAM] COMPLETE', { _id: streamId }); + } catch (error) { + console.error('[ASYNC - STREAM] ERROR', error); + await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.error } }); + } + } else { + console.error('[ASYNC - STREAM] ERROR', 'No new stream found', streamId); + } +} diff --git a/lib/seed/devops/serverless-admin.yaml b/lib/seed/devops/serverless-admin.yaml new file mode 100644 index 0000000..d9e652a --- /dev/null +++ b/lib/seed/devops/serverless-admin.yaml @@ -0,0 +1,55 @@ +app: ${file(./package.json):name} +service: admin +package: + individually: true +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + AWS_Hooks: hooks-${self:provider.stage}-hookHandler + S3_BUCKET: ${self:app}-appdata + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' + - Effect: Allow + Action: + - s3:* + Resource: arn:aws:s3:::${self:provider.environment.S3_BUCKET}/* +plugins: + - serverless-webpack + - serverless-domain-manager +custom: + stage: ${opt:stage, self:provider.stage} + domains: ${file(./config/domains.yaml):domains} + customDomain: ${file(./config/domains.yaml):customDomain} + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk + - puppeteer +functions: + admin: + handler: handler.adminHandler + timeout: 60 + events: + - http: + path: / + method: post + cors: + origin: '*' + headers: + - Content-Type + - Authorization + - X-Api-Key + - X-Amz-Security-Token + - X-Amz-User-Agent + - organisationid + + - http: + path: / + method: get + cors: true diff --git a/lib/seed/devops/serverless-app.yaml b/lib/seed/devops/serverless-app.yaml new file mode 100644 index 0000000..b15e7ab --- /dev/null +++ b/lib/seed/devops/serverless-app.yaml @@ -0,0 +1,60 @@ +app: ${file(./package.json):name} +service: app +package: + individually: true +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + AWS_Hooks: hooks-${self:provider.stage}-hookHandler + S3_BUCKET: ${self:app}-appdata + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' + - Effect: Allow + Action: + - s3:* + Resource: arn:aws:s3:::${self:provider.environment.S3_BUCKET}/* + +plugins: + - serverless-webpack + - serverless-domain-manager + - serverless-offline +custom: + stage: ${opt:stage, self:provider.stage} + domains: ${file(./config/domains.yaml):domains} + customDomain: ${file(./config/domains.yaml):customDomain} + serverless-offline: + httpPort: 4000 + lambdaPort: 4010 + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk + - puppeteer +functions: + app: + handler: handler.appHandler + timeout: 60 + events: + - http: + path: / + method: post + cors: + origin: '*' + headers: + - Content-Type + - Authorization + - X-Authorization + - X-Api-Key + - X-Amz-Security-Token + - X-Amz-User-Agent + - organisationid + - http: + path: / + method: get + cors: true diff --git a/lib/seed/devops/serverless-domains.yaml b/lib/seed/devops/serverless-domains.yaml new file mode 100644 index 0000000..9008ff8 --- /dev/null +++ b/lib/seed/devops/serverless-domains.yaml @@ -0,0 +1,12 @@ +app: ${file(./package.json):name} +service: domains +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x +plugins: + - serverless-domain-manager +custom: + stage: ${opt:stage, self:provider.stage} + domains: ${file(./config/domains.yaml):domains} + customDomain: ${file(./config/domains.yaml):customDomain} \ No newline at end of file diff --git a/lib/seed/devops/serverless-hooks.yaml b/lib/seed/devops/serverless-hooks.yaml new file mode 100644 index 0000000..044d425 --- /dev/null +++ b/lib/seed/devops/serverless-hooks.yaml @@ -0,0 +1,37 @@ +app: ${file(./package.json):name} +service: hooks +package: + individually: true +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + AWS_Hooks: hooks-${self:provider.stage}-hookHandler + S3_BUCKET: ${self:app}-appdata + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' + - Effect: Allow + Action: + - s3:* + Resource: arn:aws:s3:::${self:provider.environment.S3_BUCKET}/* + +plugins: + - serverless-webpack +custom: + stage: ${opt:stage, self:provider.stage} + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk + - puppeteer +functions: + hookHandler: + handler: handler.hookHandler + memorySize: 1024 # optional, in MB, default is 1024 + timeout: 600 # optional, in seconds, default is 6 + async: true diff --git a/lib/seed/engine/EngineAccessService.ts b/lib/seed/engine/EngineAccessService.ts new file mode 100644 index 0000000..b1e6652 --- /dev/null +++ b/lib/seed/engine/EngineAccessService.ts @@ -0,0 +1,161 @@ +import { AccountTypeEnum } from '@src/accounts/account.components'; +import AccountModel from '@src/accounts/account.model'; +import { newError } from '@seed/helpers/Error'; +import _ from 'lodash'; + +export class EngineAccessService { + static addPermissions(ressource: any, type: ('r' | 'w' | 'd')[], ids: (string | string)[]): void { + for (let index = 0; index < type.length; index++) { + const t = type[index]; + ressource.permissions[t] = ressource.permissions[t].concat(ids); + } + } + + static addPermissionToQuery(account: AccountModel | null, query: 'get' | 'update' | 'delete', params: any): any { + let types: any[] = ['public']; + + if (account && account.types) { + /* + * Verify if admin, no need to add the query filters + */ + if (account.types.includes(AccountTypeEnum.admin)) return params; + + /* + * Verify if organisation type of access + */ + + // if (account.organisationIds) { + // params.organisationId = { $in: account.organisationIds }; + // } + + /* + * Add the account id and type + */ + + types.push(account._id); + types = types.concat(account.types); + } + + if (!params.$and) params.$and = []; + + switch (query) { + default: + case 'get': + params.$and.push({ 'permissions.r': { $in: types } }); + break; + case 'update': + params.$and.push({ 'permissions.w': { $in: types } }); + break; + case 'delete': + params.$and.push({ 'permissions.d': { $in: types } }); + break; + } + + return params; + } + + static checkPermissions(ressource: any, account: AccountModel | null, type: 'c' | 'r' | 'w' | 'd'): boolean { + // Adding the public by default + let perm: any[] = [AccountTypeEnum.public]; + + if (account && account.types) { + /* + * Verify if admin + */ + if (account.types.includes(AccountTypeEnum.admin)) return true; + + /* + * Verify if organisation type of access + */ + if ( + ressource.organisationId && + account.organisationIds && + !account.organisationIds.includes(ressource.organisationId) && + !ressource.permissions.r.includes(AccountTypeEnum.public) + ) + throw newError(2100, { + allowed: ressource.organisationId, + you: account.organisationIds, + }); + + /* + * Add the account id and type + */ + perm.push(account._id); + perm = perm.concat(account.types); + } + + /* + * Verify on the ressource level + */ + + const permissions = ressource.permissions[type]; + + // if there is no permission on the ressource, return true + if (!permissions) return true; + + let hasPerm = false; + + // Verifying if it matches + for (let index = 0; index < perm.length; index++) { + const element = perm[index]; + if (permissions.includes(element)) { + hasPerm = true; + break; + } + } + + if (!hasPerm) + throw newError(2000, { + allowed: permissions, + you: { _id: account?._id, types: account?.types }, + }); + + return true; + } + + static checkOrganisationPermissions(ressource: any, organisationId: string): boolean { + if (organisationId == ressource.organisationId || organisationId == ressource._id) return true; + throw newError(2100, { allowedOrgId: ressource.organisationId, youOrgId: organisationId }); + } + + static addOrganisationToQuery(account: AccountModel | null, query: 'get' | 'update' | 'delete', params: any): any { + let types: any[] = ['public']; + + if (account && account.types) { + /* + * Verify if admin, no need to add the query filters + */ + if (account.types.includes(AccountTypeEnum.admin)) return params; + + /* + * Verify if organisation type of access + */ + if (account.organisationIds) { + params.organisationId = { $in: account.organisationIds }; + } + + /* + * Add the account id and type + */ + + types.push(account._id); + types = types.concat(account.types); + } + + switch (query) { + default: + case 'get': + params = { ...params, $or: [{ 'permissions.r': { $in: _.uniq(types) } }] }; + break; + case 'update': + params = { ...params, $or: [{ 'permissions.w': { $in: _.uniq(types) } }] }; + break; + case 'delete': + params = { ...params, $or: [{ 'permissions.d': { $in: _.uniq(types) } }] }; + break; + } + + return params; + } +} diff --git a/lib/seed/engine/EngineModel.ts b/lib/seed/engine/EngineModel.ts new file mode 100644 index 0000000..748256f --- /dev/null +++ b/lib/seed/engine/EngineModel.ts @@ -0,0 +1,277 @@ +import _ from 'lodash'; + +import { ObjectType } from 'type-graphql'; +import { classToPlain, Exclude, plainToClassFromExist } from 'class-transformer'; + +import { Collection, ObjectId } from 'mongodb'; +import DB from '@seed/services/database/DBService'; + +import { Permission } from '@seed/interfaces/permission'; +import { ApolloContext } from '@seed/interfaces/context'; + +import { IEngineSchema, MetaBy } from './EngineSchema'; + +import { GetArgs } from '@seed/graphql/Request'; + +import { + bulk, + deleteOne, + getAll, + getCount, + getMany, + getOne, + getQuery, + saveMany, + saveOne, + updateOne, + updateOneCustom, + updateOneSubField, +} from './utils/crud.utils'; +import { getCountGeneric, getManyGeneric, getManyGenericWithArgs, getOneGeneric } from './utils/service.utils'; + +import { + BulkInput, + DeleteOneInput, + GetAllInput, + GetCountInput, + GetManyInput, + GetOneInput, + GetQueryInput, + SaveManyInput, + SaveOneInput, + UpdateOneCustomInput, + UpdateOneInput, + UpdateOneSubField, +} from './utils/__interface'; +import { EnginePathComponent } from '@seed/interfaces/components'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { nullToUndefined } from '@seed/helpers/Utils'; + +@ObjectType() +export abstract class EngineModel implements IEngineSchema { + @Exclude() + public collectionName: ModelCollectionEnum | string; + @Exclude() + public permissions: Permission; + + @Exclude() + defaultSort: string; + + dbData: SchemaDB; + + searchT?: string; + + _id: string; + organisationId?: string | undefined; + paths?: EnginePathComponent[]; + by?: MetaBy | undefined; + createdAt: Date; + updatedAt: Date; + + tagsIds?: string[]; + + public constructor(init: { + collectionName: ModelCollectionEnum | string; + permissions: Permission; + dataInit: SchemaDB & Partial; + defaultSort?: string; + }) { + this.collectionName = init.collectionName; + this.permissions = init.permissions; + + this.dbData = init.dataInit; + + this.permissions = { + c: init.permissions.c, + r: init.permissions.r, + w: init.permissions.w, + d: init.permissions.d, + }; + + this.createdAt = new Date(); + this.updatedAt = new Date(); + + this.defaultSort = init.defaultSort || 'createdAt desc'; + } + + /* + * + * Model functions + * + */ + + public get(): Schema { + const data: any = this.dbData; + + if (this._id) data._id = this._id; + if (this.permissions) data.permissions = this.permissions; + + if (this.organisationId) data.organisationId = this.organisationId; + if (this.by) data.by = this.by; + + if (this.createdAt) data.createdAt = this.createdAt; + if (this.updatedAt) data.updatedAt = this.updatedAt; + + return this.plainToClass(data) as Schema; + } + + public set(doc: any): void { + const newData = { + ...classToPlain(this.dbData), // Get old data + ...doc, // Replace with new data + }; + + this.dbData = _.assign(this.dbData, newData); + + if (doc._id) this._id = doc._id; + + if (doc.permissions) { + this.permissions = { + c: _.uniq(_.concat(doc.permissions.c)), + r: _.uniq(_.concat(doc.permissions.r)), + w: _.uniq(_.concat(doc.permissions.w)), + d: _.uniq(_.concat(doc.permissions.d)), + }; + } + + if (doc.organisationId) this.organisationId = doc.organisationId; + + if (doc.paths) this.paths = doc.paths; + + if (doc.createdAt) this.createdAt = doc.createdAt; + if (doc.updatedAt) this.updatedAt = doc.updatedAt; + } + + async beforeCreate?(ctx?: ApolloContext | null): Promise; + async beforeUpdate?(ctx?: ApolloContext | null): Promise; + async beforeDelete?(ctx?: ApolloContext | null): Promise; + + async afterCreate?(changeStream: any): Promise; + async afterUpdate?(changeStream: any): Promise; + async afterDelete?(changeStream: any): Promise; + + /* + * + * Search Functions & Helpers + * + */ + + searchEngine?(): any; + searchText?(): any; + /* + * + * Transform Functions + * + */ + + abstract plainToClass(plain: any): Schema | Schema[]; + + /* + * + * DB Functions + * + */ + + public async db(): Promise> { + return await (await DB.getInstance()).db.collection(this.collectionName); + } + + generatePaths?(): EnginePathComponent[]; + + public getPaths(): EnginePathComponent[] { + return this.generatePaths ? this.generatePaths() : this.paths ? this.paths : []; + } + + public getFullPath(): EnginePathComponent[] { + const myPath = this.getPaths(); + myPath.push({ + ressourceModel: this.collectionName, + ressourceId: this._id, + }); + + return myPath; + } + + // ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + // ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + // ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + // ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + // ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + // ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + + public async getQuery(input: GetQueryInput): Promise { + return getQuery(this, input); + } + public async getMany(input: GetManyInput): Promise { + return getMany(this, input); + } + public async getCount(input: GetCountInput): Promise { + return getCount(this, input); + } + public async getAll(input: GetAllInput): Promise { + return getAll(this, input); + } + public async getOne(input: GetOneInput): Promise { + let newInput; + + if (!input.query) { + newInput = { query: input }; + } else newInput = input; + + return getOne(this, newInput); + } + + // ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗ + // ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║ + // ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║ + // ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║ + // ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║ + // ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ + + public async saveOne(input: SaveOneInput): Promise { + return saveOne(this, input); + } + public async updateOne(input: UpdateOneInput): Promise { + return updateOne(this, input); + } + public async updateOneCustom(input: UpdateOneCustomInput): Promise { + return updateOneCustom(this, input); + } + public async updateOneSubField(input: UpdateOneSubField): Promise { + return updateOneSubField(this, input); + } + + public async deleteOne(input: DeleteOneInput): Promise { + return deleteOne(this, input); + } + + public async saveMany(input: SaveManyInput): Promise { + return saveMany(this, input); + } + + public async bulk(input: BulkInput): Promise { + return bulk(this, input); + } + + // ██████╗ ███████╗███╗ ██╗███████╗██████╗ ██╗ ██████╗███████╗ + // ██╔════╝ ██╔════╝████╗ ██║██╔════╝██╔══██╗██║██╔════╝██╔════╝ + // ██║ ███╗█████╗ ██╔██╗ ██║█████╗ ██████╔╝██║██║ ███████╗ + // ██║ ██║██╔══╝ ██║╚██╗██║██╔══╝ ██╔══██╗██║██║ ╚════██║ + // ╚██████╔╝███████╗██║ ╚████║███████╗██║ ██║██║╚██████╗███████║ + // ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚══════ + + public async getOneGeneric(id: string, ctx: ApolloContext): Promise { + return getOneGeneric(this, id, ctx); + } + public async getManyGeneric(query: any, pagination: GetArgs | undefined, ctx: ApolloContext | null): Promise { + return getManyGeneric(this, query, pagination, ctx); + } + + public async getCountGeneric(baseArguments: any, ctx: ApolloContext): Promise { + return getCountGeneric(this, baseArguments, ctx); + } + + public async getManyGenericWithArgs(baseArguments: any, ctx: ApolloContext | null): Promise { + return getManyGenericWithArgs(this, baseArguments, ctx); + } +} diff --git a/lib/seed/engine/EngineSchema.ts b/lib/seed/engine/EngineSchema.ts new file mode 100644 index 0000000..8e6c159 --- /dev/null +++ b/lib/seed/engine/EngineSchema.ts @@ -0,0 +1,58 @@ +import { EnginePathComponent } from '@seed/interfaces/components'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { Field, ID, ObjectType, Authorized, InterfaceType } from 'type-graphql'; + +@ObjectType() +export class MetaBy { + @Field(() => ID, { nullable: true }) + createdBy?: string; + + @Field(() => ID, { nullable: true }) + updatedBy?: string; + + @Field(() => ID, { nullable: true }) + deletedBy?: string; +} + +@ObjectType() +export class MetaPermissions { + @Field(() => [String]) + r: string[]; + + @Authorized(AccountTypeEnum.admin) + @Field(() => [String]) + w: string[]; + + @Authorized(AccountTypeEnum.admin) + @Field(() => [String]) + d: string[]; +} + +@InterfaceType() +export abstract class IEngineSchema { + @Field(() => ID) + _id: string; + + @Field(() => ID, { nullable: true }) + organisationId?: string; + + @Field(() => [EnginePathComponent], { nullable: true }) + paths?: EnginePathComponent[]; + + searchT?: string; + + @Field(() => MetaBy, { nullable: true }) + by?: MetaBy; + + @Field(() => MetaPermissions, { nullable: true }) + permissions: MetaPermissions; + + @Field() + createdAt: Date; + + @Field() + updatedAt: Date; + + @Field(() => [String], { nullable: true }) + tagsIds?: string[]; +} diff --git a/lib/seed/engine/decorators/db.guard.ts b/lib/seed/engine/decorators/db.guard.ts new file mode 100644 index 0000000..99233ec --- /dev/null +++ b/lib/seed/engine/decorators/db.guard.ts @@ -0,0 +1,40 @@ +import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import DB from '@seed/services/database/DBService'; + +import { newError } from '@seed/helpers/Error'; + +@ValidatorConstraint({ async: true }) +export class IsRefExistConstraint implements ValidatorConstraintInterface { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + async validate(ressourceId: any, args: ValidationArguments & any) { + // Validate only when in the DB object + if (args.targetName.includes('DB')) { + const obj = args.object as any; + // Check what kind of id check + if (obj.ressourceId && obj.ressourceModel) { + + const res = await (await DB.getInstance()).db.collection(ModelCollectionEnum[obj.ressourceModel]).findOne({ _id: obj.ressourceId }); + if (!res) throw newError(4040, obj); + } else { + } + } + + args.hasValidated = true; + + return true; + } +} + +export function IsRefExist(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + return function(object: Record, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsRefExistConstraint, + }); + }; +} diff --git a/lib/seed/engine/decorators/pre.hooks.ts b/lib/seed/engine/decorators/pre.hooks.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/seed/engine/genericResolvers/BaseEngineResolver.ts b/lib/seed/engine/genericResolvers/BaseEngineResolver.ts new file mode 100644 index 0000000..5ebe023 --- /dev/null +++ b/lib/seed/engine/genericResolvers/BaseEngineResolver.ts @@ -0,0 +1,114 @@ +import { Query, Arg, Resolver, Ctx, Args, Mutation, ClassType } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; +import { EngineMiddleware, EngineMiddlewareInput } from '@seed/graphql/MiddlewareV2'; +import { EngineModel } from '../EngineModel'; + +export const createEngineQueryResolver = , T extends ClassType, ARGS extends ClassType>(init: { + domain: string; + schemaName: U; + modelName: T; + argsType: ARGS; + engineMiddleware: EngineMiddlewareInput; +}): any => { + @Resolver({ isAbstract: true }) + abstract class BaseQueryResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => init.schemaName, { name: `${init.domain}GetOne` }) + @EngineMiddleware(init.engineMiddleware) + async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName() as EngineModel; + const result = await model.getOneGeneric(id, ctx); + + return result; + } + + @Query(() => [init.schemaName], { name: `${init.domain}GetMany` }) + @EngineMiddleware(init.engineMiddleware) + async getMany(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName() as EngineModel; + return model.getManyGenericWithArgs(args, ctx); + } + + @Query(() => Number, { name: `${init.domain}GetCount` }) + @EngineMiddleware(init.engineMiddleware) + async getCount(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName() as EngineModel; + return model.getCountGeneric(args, ctx); + } + } + + return BaseQueryResolver; +}; + +export const createEngineMutationResolver = < + U extends ClassType, + T extends ClassType, + NEW extends ClassType, + EDIT extends ClassType +>(init: { + domain: string; + schemaName: U; + modelName: T; + newInput: NEW; + editInput: EDIT; + engineMiddleware: EngineMiddlewareInput; + validate?: boolean; +}): any => { + @Resolver({ isAbstract: true }) + abstract class BaseMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => init.schemaName, { name: `${init.domain}AddOne` }) + @EngineMiddleware({ ...init.engineMiddleware, validations: [{ schema: init.newInput }] }) + async addOne(@Arg('input', () => init.newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName() as EngineModel; + return await await model.saveOne({ newData: input, ctx }); + } + + @Mutation(() => init.schemaName, { name: `${init.domain}EditOne` }) + @EngineMiddleware({ ...init.engineMiddleware, validations: [{ schema: init.editInput }] }) + async editOne(@Arg('id') id: string, @Arg('input', () => init.editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName() as EngineModel; + return await await model.updateOne({ + query: { _id: id }, + newData: input, + ctx: ctx, + }); + } + + @Mutation(() => init.schemaName, { name: `${init.domain}DeleteOne` }) + @EngineMiddleware({ ...init.engineMiddleware }) + async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName() as EngineModel; + return await await model.deleteOne({ + query: { _id: id }, + ctx: ctx, + }); + } + } + + return BaseMutationResolver; +}; diff --git a/lib/seed/engine/utils/__interface.ts b/lib/seed/engine/utils/__interface.ts new file mode 100644 index 0000000..bf6bfbe --- /dev/null +++ b/lib/seed/engine/utils/__interface.ts @@ -0,0 +1,76 @@ +import { GetArgs } from '@seed/graphql/Request'; +import { ApolloContext } from '@seed/interfaces/context'; +import { FilterQuery, UpdateQuery } from 'mongodb'; +import { EngineModel } from '../EngineModel'; +import { IEngineSchema } from '../EngineSchema'; + +export interface GetQueryInput { + query: FilterQuery; + ctx?: ApolloContext | null; +} + +export interface GetManyInput { + query: FilterQuery; + pagination?: GetArgs; + ctx?: ApolloContext | null; +} +export interface GetAllInput { + query: FilterQuery; + ctx?: ApolloContext | null; +} +export interface GetCountInput { + query: FilterQuery; + ctx?: ApolloContext | null; +} + +export interface GetOneInput { + query: FilterQuery; + ctx?: ApolloContext | null; +} + +export interface SaveOneInput { + newData?: SchemaDBInterface & Partial; + additionnalData?: Partial; + ctx?: ApolloContext | null; + upsert?: boolean; +} + +export interface SaveManyInput { + models: EngineModel[]; + ctx?: ApolloContext | null; +} + +export interface BulkInput { + inserts?: EngineModel[]; + updates?: { + query: FilterQuery; + newData?: Partial; + updateRequest?: UpdateQuery; + }[]; + ctx?: ApolloContext | null; +} + +export interface UpdateOneInput { + query: FilterQuery; + newData: Partial; + ctx?: ApolloContext | null; +} + +export interface UpdateOneCustomInput { + query: FilterQuery; + updateRequest: UpdateQuery; + ctx?: ApolloContext | null; +} + +export interface UpdateOneSubField { + query: FilterQuery; + fieldName: keyof SchemaDBInterface; + subPath?: string; + fieldValue: any; + ctx?: ApolloContext | null; +} + +export interface DeleteOneInput { + query: FilterQuery; + ctx?: ApolloContext | null; +} diff --git a/lib/seed/engine/utils/crud.utils.ts b/lib/seed/engine/utils/crud.utils.ts new file mode 100644 index 0000000..f9886c3 --- /dev/null +++ b/lib/seed/engine/utils/crud.utils.ts @@ -0,0 +1,721 @@ +import { parsePaginationOptions } from '@lib/seed/helpers/Request'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { validateOrReject } from 'class-validator'; +import { EngineAccessService } from '../EngineAccessService'; +import { EngineModel } from '../EngineModel'; +import { IEngineSchema } from '../EngineSchema'; + +import { v4 as uuid } from 'uuid'; +import { + BulkInput, + DeleteOneInput, + GetAllInput, + GetCountInput, + GetManyInput, + GetOneInput, + GetQueryInput, + SaveManyInput, + SaveOneInput, + UpdateOneCustomInput, + UpdateOneInput, + UpdateOneSubField, +} from './__interface'; +import DB from '@lib/seed/services/database/DBService'; +import { createChangeStream } from './streams/stream.service'; +import { StreamOperationType } from './streams/schemas/stream.components'; +import { BulkWriteOperation, ObjectId } from 'mongodb'; +import { newError } from '@seed/helpers/Error'; +import { clog } from '@seed/helpers/Utils'; + +export function getQuery( + model: EngineModel, + input: GetQueryInput, +): any { + let finalQuery: any; + const { query, ctx } = input; + + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + // We add the permission (on r field) to make sure that kind of user has access to the ressource + if (!ctx.ctx.noPermissionCheck) finalQuery = EngineAccessService.addPermissionToQuery(account, 'get', { ...query }); + + // We check if it comes from an organisation + // Check of the user's correct authorisation on that organisation comes from the middleware + if (!ctx.ctx.noOrganisationCheck) { + if (organisationId) { + if (model.collectionName == 'accounts') finalQuery['organisationIds'] = organisationId; + else finalQuery['organisationId'] = { $eq: organisationId }; + } else { + // If admin & public query -> do not add model condition and let search every records) + if (account && account.types && !account.types.includes(AccountTypeEnum.public) && !account.types.includes(AccountTypeEnum.admin)) { + finalQuery['organisationId'] = { $exists: false }; + } + } + } + } else { + finalQuery = { ...query }; + } + + return finalQuery; +} + +export const getMany = async ( + model: EngineModel, + input: GetManyInput, +): Promise => { + let { pagination } = input; + + const finalQuery = getQuery(model, input); + + if (!pagination) pagination = { limit: 100, skip: 0 }; + if (!pagination.sort) pagination.sort = model.defaultSort; + const paginationOption = parsePaginationOptions(pagination); + + try { + console.log('search request', JSON.stringify(finalQuery, null, 2)); + const dbResults = await (await model.db()).find(finalQuery, paginationOption).toArray(); + return model.plainToClass(dbResults) as any; + } catch (error) { + return []; + } +}; + +export const getCount = async ( + model: EngineModel, + input: GetCountInput, +): Promise => { + const finalQuery = getQuery(model, input); + + try { + const result = (await (await model.db()).countDocuments(finalQuery)) as any; + return result; + } catch (error) { + return 0; + } +}; + +export const getAll = async ( + model: EngineModel, + input: GetAllInput, +): Promise => { + const finalQuery = getQuery(model, input); + try { + clog(finalQuery); + const res = await (await model.db()).find(finalQuery).toArray(); + return model.plainToClass(res); + } catch (error) { + return []; + } +}; + +export const getOne = async ( + model: EngineModel, + input: GetOneInput, +): Promise => { + const { query, ctx } = input; + + let dataToReturn; + if (query._id) { + const _id = query._id as any; + if (_id.match(/^[0-9a-fA-F]{24}$/)) { + query._id = new ObjectId(_id) as any; + } + } + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + const doc = await (await model.db()).findOne(query); + if (!doc) throw await newError(4041, { id: query._id, model: model.collectionName }); + + dataToReturn = model.plainToClass(doc) as any; + model.set(dataToReturn); + + if (!ctx.ctx.noOrganisationCheck) { + if (organisationId) { + if (model.collectionName != 'accounts') EngineAccessService.checkOrganisationPermissions(model, organisationId); + } + } + if (!ctx.ctx.noPermissionCheck) EngineAccessService.checkPermissions(dataToReturn, account, 'r'); + } else { + const doc = await (await model.db()).findOne(query); + if (!doc) throw await newError(4041, { id: query._id, model: model.collectionName }); + + dataToReturn = model.plainToClass(doc) as any; + model.set(dataToReturn); + } + + return model.get(); +}; + +export const saveOne = async ( + model: EngineModel, + input: SaveOneInput, +): Promise => { + const { newData, additionnalData, upsert } = input; + let ctx = input.ctx; + + if (newData) model.set(newData); + if (additionnalData) model.set(additionnalData); + + // Set the path + model.set({ paths: model.getPaths() }); + + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + EngineAccessService.addPermissions(model, ['r', 'w', 'd'], [account._id]); + + // TODO : Check create permissions + + if (organisationId) { + model.set({ organisationId }); + } + + if (model.by) model.by.createdBy = account._id; + else { + model.by = { + createdBy: account._id, + }; + } + } + + // if (!ctx) ctx = await createApolloContext(); + + try { + if (model.beforeCreate) await model.beforeCreate(ctx); + await validateOrReject(model.dbData as any); + } catch (error) { + throw error; + } + + const dataToSave = model.get() as any; + if (model.searchText) dataToSave.searchT = model.searchText(); + + if (!model._id) model._id = uuid(); + + delete dataToSave._id; + + try { + const result = await (await model.db()).findOneAndUpdate( + { _id: model._id } as any, + { + $set: dataToSave, + $setOnInsert: { + _id: model._id, + }, + } as any, + { upsert: upsert ? upsert : true, returnOriginal: false }, + ); + + if (result.ok == 0) throw await newError(1000, result); + + await createChangeStream(model, StreamOperationType.insert); + + return model.get(); + } catch (error) { + throw await newError(1000, error); + } +}; + +export const updateOne = async ( + model: EngineModel, + input: UpdateOneInput, +): Promise => { + try { + const { query, newData, ctx } = input; + + if (!model._id) await model.getOne(input); + if (newData) model.set(newData); + + model.set({ paths: model.getPaths() }); + + if (ctx) { + const account = ctx.ctx.user; + // Check if doc exists & correct permissions + + if (model.by) model.by.updatedBy = account._id; + else { + model.by = { + updatedBy: account._id, + }; + } + + EngineAccessService.checkPermissions(model, account, 'w'); + } + + if (model.beforeUpdate) await model.beforeUpdate(ctx); + + const dataToSave = model.get() as any; + if (model.searchText) dataToSave.searchT = model.searchText(); + + const result = await (await model.db()).findOneAndUpdate(query, { $set: dataToSave }, { upsert: false, returnOriginal: false }); + if (result.ok != 1) throw await newError(1000, result); + + model.set(result.value); + + await createChangeStream(model, StreamOperationType.update); + + return model.get(); + } catch (error) { + throw error; + } +}; + +export const updateOneCustom = async ( + model: EngineModel, + input: UpdateOneCustomInput, +): Promise => { + try { + const { query, updateRequest, ctx } = input; + if (!model._id) await model.getOne(input); + + if (ctx) { + const account = ctx.ctx.user; + // Check if doc exists + // dataBeforeChange = model.get(); + + EngineAccessService.checkPermissions(model, account, 'w'); + + if (model.by) model.by.updatedBy = account._id; + else { + model.by = { + updatedBy: account._id, + }; + } + + // Validate new data that will go into the DB + // const validation = await model.validate(); + // if (!validation.success) return validation; + } + + // Prepare data + model.updatedAt = new Date(); + + const result = await (await model.db()).findOneAndUpdate(query, updateRequest, { upsert: false, returnOriginal: false }); + if (result.ok != 1) throw await newError(1000, result); + + if (result.value) model.set(result.value); + + await createChangeStream(model, StreamOperationType.update); + + return model.get(); + } catch (error) { + throw error; + } +}; + +export const deleteOne = async ( + model: EngineModel, + input: DeleteOneInput, +): Promise => { + const { query, ctx } = input; + try { + if (!model._id) await model.getOne(input); + if (model.beforeDelete) await model.beforeDelete(ctx); + + if (ctx) { + const account = ctx.ctx.user; + // Check if doc exists & correct permissions + EngineAccessService.checkPermissions(model, account, 'd'); + + if (model.by) model.by.deletedBy = account._id; + else { + model.by = { + deletedBy: account._id, + }; + } + } + + const result = await (await model.db()).findOneAndDelete(query); + if (result.ok != 1) throw await newError(1001); + + const deletedModel = '_deleted_' + model.collectionName; + await (await DB.getInstance()).db.collection(deletedModel).insertOne(model.get()); + + await createChangeStream(model, StreamOperationType.delete); + + return model.get(); + } catch (error) { + throw error; + } +}; + +export const saveMany = async ( + model: EngineModel, + input: SaveManyInput, +): Promise => { + const ctx = input.ctx; + const models = input.models; + + const dataToSaves: any[] = []; + for (let index = 0; index < models.length; index++) { + const element = models[index]; + + // Set the path + element.set({ paths: element.getPaths() }); + + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + EngineAccessService.addPermissions(element, ['r', 'w', 'd'], [account._id]); + + // TODO : Check create permissions + + if (organisationId) { + element.set({ organisationId }); + } + if (element.by) element.by.createdBy = account._id; + else { + element.by = { + createdBy: account._id, + }; + } + try { + if (element.beforeCreate) await element.beforeCreate(ctx); + await validateOrReject(element.dbData); + } catch (error) { + throw error; + } + } + + const dataToSave = element.get() as any; + if (element.searchText) dataToSave.searchT = element.searchText(); + if (!element._id) dataToSave._id = uuid(); + + dataToSaves.push(dataToSave); + } + + try { + const result = await (await model.db()).insertMany(dataToSaves, { ordered: true }); + + if (result.result.ok == 0) throw await newError(1000, result); + + // await createChangeStream(model, StreamOperationType.insert); + + return dataToSaves; + } catch (error) { + throw await newError(1000, error); + } +}; + +export const bulk = async ( + model: EngineModel, + input: BulkInput, +): Promise => { + const { inserts, updates, ctx } = input; + + const bulkOps: BulkWriteOperation[] = []; + if (inserts) { + inserts.forEach((element) => { + bulkOps.push({ + insertOne: { + document: element.get() as any, + }, + }); + }); + } + + if (updates) { + updates.forEach((element) => { + if (element.newData) + bulkOps.push({ + updateOne: { + filter: element.query, + update: { $set: element.newData }, + }, + }); + else if (element.updateRequest) + bulkOps.push({ + updateOne: { + filter: element.query, + update: element.updateRequest, + }, + }); + }); + } + + const bOpsArray: BulkWriteOperation[][] = []; + if (bulkOps.length > 0) { + const chunk = 1000; + let i, j; + + for (i = 0, j = bulkOps.length; i < j; i += chunk) { + bOpsArray.push(bulkOps.slice(i, i + chunk)); + } + } else bOpsArray.push(bulkOps); + + try { + for (let index = 0; index < bOpsArray.length; index++) { + const element = bOpsArray[index]; + const res = await (await model.db()).bulkWrite(element); + + console.log(res); + } + } catch (error) { + throw await newError(1000, error); + } + + return; +}; + +export const updateOneSubField = async ( + model: EngineModel, + input: UpdateOneSubField, +): Promise => { + const { fieldName, fieldValue, subPath, query, ctx } = input; + + let updatePath = fieldName as string; + if (subPath) updatePath += subPath; + try { + const filterQ = { + ...query, + }; + return await model.updateOneCustom({ + query: filterQ, + updateRequest: { + $set: { + [`${updatePath}`]: fieldValue, + } as any, + }, + ctx, + }); + } catch (error) { + throw error; + } +}; + +// public async saveMany(newDataArr: Partial[], ctx: ApolloContext | null): Promise { +// // Initialize the change stream +// const changeStreams: any[] = []; +// const afterCreatePromises: any[] = []; + +// const prehooksPromises = newDataArr.map(async (newData) => { +// const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this); +// if (ctx) { +// const account = ctx.ctx.user; +// const organisationId = ctx.ctx.organisationId; + +// addPermissions(clone, ['r', 'w', 'd'], [account._id]); + +// if (organisationId) { +// newData.organisationId = organisationId; +// } + +// newData.createdBy = account._id; +// } + +// clone.set(newData); + +// if (!ctx) ctx = await createApolloContext(); + +// if (clone.prehook) await clone.prehook(); +// if (clone.beforeCreate) await clone.beforeCreate(ctx); + +// const { _id, ...dataToSave } = clone.get(); +// const savedId = _id ? _id : uuid(); +// clone.set({ _id: savedId }); + +// const changeStreamId = uuid(); +// const changeStreamData = { +// _id: changeStreamId, +// operation: StreamOperationType.insert, +// collection: clone.collectionName, +// documentKey: clone._id, +// insertedValues: clone.get(), +// hookStatus: PostHookStatus.new, +// createdAt: new Date(), +// updateAt: new Date(), +// }; + +// changeStreams.push(changeStreamData); + +// // eslint-disable-next-line @typescript-eslint/no-this-alias +// afterCreatePromises.push(async () => { +// if (clone.afterCreate) { +// const res = await asyncHook(changeStreamData); + +// if (res) { +// await clone.afterCreate(changeStreamData); + +// await (await DB.getInstance()).db +// .collection('stream.changes') +// .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); +// } +// } +// }); + +// return clone.get(); +// }); + +// try { +// const dataArr = await Promise.all(prehooksPromises); +// await (await this.db()).insertMany(dataArr, { ordered: false }); +// await (await DB.getInstance()).db.collection('stream.changes').insertMany(changeStreams, { ordered: false }); +// await promiseAll(afterCreatePromises); +// return dataArr; +// } catch (error) { +// console.error(error); +// throw await newError('db.error', error); +// } +// } + +// ██████╗ ███████╗██████╗ ███╗ ███╗██╗███████╗███████╗██╗ ██████╗ ███╗ ██╗███████╗ +// ██╔══██╗██╔════╝██╔══██╗████╗ ████║██║██╔════╝██╔════╝██║██╔═══██╗████╗ ██║██╔════╝ +// ██████╔╝█████╗ ██████╔╝██╔████╔██║██║███████╗███████╗██║██║ ██║██╔██╗ ██║███████╗ +// ██╔═══╝ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║╚════██║╚════██║██║██║ ██║██║╚██╗██║╚════██║ +// ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║███████║███████║██║╚██████╔╝██║ ╚████║███████║ +// ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ + +// public async updateQueryWithPermission(query: FilterQuery, ctx: ApolloContext): Promise> { +// const account = ctx.ctx.user; +// const organisationId = ctx.ctx.organisationId; + +// // We add the permission (on r field) to make sure that kind of user has access to the ressource +// const finalQuery = addPermissionToQuery(account, 'get', { ...query }); + +// // We check if it comes from an organisation +// // Check of the user's correct authorisation on that organisation comes from the middleware +// if (organisationId) { +// finalQuery['organisationId'] = { $eq: organisationId }; +// } else { +// finalQuery['organisationId'] = { $exists: false }; +// } + +// return finalQuery; +// } + +// ███████╗██╗ ██╗██████╗ ██████╗ ███████╗███████╗███████╗ +// ██╔════╝██║ ██║██╔══██╗ ██╔══██╗██╔════╝██╔════╝██╔════╝ +// ███████╗██║ ██║██████╔╝█████╗██████╔╝█████╗ ███████╗███████╗ +// ╚════██║██║ ██║██╔══██╗╚════╝██╔══██╗██╔══╝ ╚════██║╚════██║ +// ███████║╚██████╔╝██████╔╝ ██║ ██║███████╗███████║███████║ +// ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ + +// public async addOneSubModel( +// parentQ: { _id: string }, +// subModelRefField: string, +// subModelFieldName: keyof this, +// subModel: SUB, +// subModelInput: any, +// ctx: ApolloContext | null, +// ): Promise { +// try { +// subModelInput[subModelRefField] = parentQ._id; +// const subM = await subModel.saveOne(subModelInput, ctx); +// await this.updateOneCustom(parentQ as any, { $push: { [`${subModelFieldName}`]: subM._id } } as any, null); +// return subM; +// } catch (error) { +// throw error; +// } +// } + +// public async editOneSubModel( +// subModel: SUB, +// subModelId: string, +// subModelInput: any, +// ctx: ApolloContext | null, +// ): Promise { +// try { +// // No need to check if belongs to the correct organisation, the middleware does that +// const subM = await subModel.updateOne({ _id: subModelId } as any, subModelInput, ctx); +// return subM; +// } catch (error) { +// throw error; +// } +// } + +// public async deleteOneSubModel( +// parentQ: FilterQuery, +// subModelFieldName: keyof this, +// subModel: SUB, +// subModelId: string, +// ctx: ApolloContext | null, +// ): Promise { +// try { +// const subM = await subModel.deleteOne({ _id: subModelId } as any, ctx); +// await this.updateOneCustom(parentQ, { $pull: { [`${subModelFieldName}`]: subM._id } } as any, null); +// return subM; +// } catch (error) { +// throw error; +// } +// } + +// public getOneSubRessource(subRessoureFieldName: keyof this | string, subRessoureId: string): any | undefined { +// try { +// const subress = _.get(this, subRessoureFieldName); +// if (subress) { +// const ind = _.findIndex(subress, { _id: subRessoureId }); +// if (ind !== -1) return subress[ind]; +// } +// return; +// } catch (error) { +// throw error; +// } +// } + +// public getDefaultSubRessource(subRessoureFieldName: keyof this | string): any | undefined { +// try { +// const subress = _.get(this, subRessoureFieldName); +// if (subress) { +// const ind = _.findIndex(subress, { default: true }); +// if (ind !== -1) return subress[ind]; +// return subress[0]; +// } +// return; +// } catch (error) { +// throw error; +// } +// } + +// public async addOneSubRessource( +// parentQ: { _id: string }, +// subRessoureFieldName: keyof this | string, +// subRessoure: any, +// ctx: ApolloContext | null, +// ): Promise { +// try { +// if (subRessoure.default && _.get(this, subRessoureFieldName)) { +// // Remove all default from others +// await this.updateOneCustom( +// { _id: this._id } as any, +// { +// $set: { [`${subRessoureFieldName}.$[].default`]: false } as any, +// }, +// ctx, +// ); +// } else subRessoure.default = false; + +// // Generate ID if not in the model +// if (!subRessoure._id) subRessoure._id = uuid(); +// await this.updateOneCustom(parentQ as any, { $push: { [`${subRessoureFieldName}`]: subRessoure } } as any, ctx); +// return subRessoure; +// } catch (error) { +// throw error; +// } +// } + +// public async deleteOneSubRessource( +// parentQ: { _id: string }, +// subRessoureFieldName: keyof this | string, +// subRessoureId: string, +// ctx: ApolloContext | null, +// ): Promise { +// try { +// if (!this.getOneSubRessource(subRessoureFieldName, subRessoureId)) throw await newError('notFound', '400'); + +// const filterQ = { +// ...parentQ, +// [`${subRessoureFieldName}._id`]: subRessoureId, +// }; + +// await this.updateOneCustom(filterQ as any, { $pull: { [`${subRessoureFieldName}`]: { _id: subRessoureId } } } as any, ctx); +// return subRessoureId; +// } catch (error) { +// throw error; +// } +// } diff --git a/lib/seed/engine/utils/service.utils.ts b/lib/seed/engine/utils/service.utils.ts new file mode 100644 index 0000000..14522e0 --- /dev/null +++ b/lib/seed/engine/utils/service.utils.ts @@ -0,0 +1,118 @@ +import { GetArgs } from '@lib/seed/graphql/Request'; +import { ApolloContext } from '@lib/seed/interfaces/context'; + +import { baseSearchFunction } from '@lib/seed/services/database/DBRequestService'; +import { EngineModel } from '../EngineModel'; + +/* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ +*/ + +export const getOneGeneric = async (model: any, id: string, ctx: ApolloContext): Promise => { + try { + return await (model as EngineModel).getOne({ query: { _id: id }, ctx }); + } catch (error) { + throw error; + } +}; +export const getManyGeneric = async (model: any, query: any, pagination: GetArgs | undefined, ctx: ApolloContext | null): Promise => { + return baseSearchFunction({ + model: model, + query: query, + pagination: pagination, + engine: true, + ctx: ctx, + }); +}; + +export const getCountGeneric = async (model: any, baseArguments: any, ctx: ApolloContext): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { pagination, ...args } = baseArguments; + + const result = await baseSearchFunction({ + model: model, + query: args, + count: true, + engine: true, + ctx: ctx, + }); + + return result; +}; + +export const getManyGenericWithArgs = async (model: any, baseArguments: any, ctx: ApolloContext | null): Promise => { + try { + const { pagination, ...args } = baseArguments; + return baseSearchFunction({ + model: model, + query: args, + pagination: pagination, + engine: true, + ctx: ctx, + }); + } catch (error) { + throw error; + } +}; + +// export const editOneGeneric = async (model: T, id: string, body: Partial, ctx: ApolloContext): Promise => { +// try { +// await model as EngineModel).updateOne({ _id: id } as any, body, ctx); +// return model as EngineModel).get(); +// } catch (error) { +// throw error; +// } +// }; + +// export const deleteOneGeneric = async (model: T, id: string, ctx: ApolloContext): Promise => { +// try { +// await model as EngineModel).deleteOne({ _id: id } as any, ctx); +// return model as EngineModel).get(); +// } catch (error) { +// throw error; +// } +// }; + +// export const permissionsAddOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise => { +// try { +// await model as EngineModel).updateOneCustom({ _id: id }, { $addToSet: { [`${input.permissionType}`]: input.permission } }, ctx); +// return model as EngineModel).get(); +// } catch (error) { +// throw error; +// } +// }; + +// export const permissionsRemoveOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise => { +// try { +// await model as EngineModel).updateOneCustom({ _id: id }, { $pull: { [`${input.permissionType}`]: input.permission } }, ctx); +// return model as EngineModel).get(); +// } catch (error) { +// throw error; +// } +// }; + +// export const publishOneGeneric = async (model: T, id: string, ctx: ApolloContext): Promise => { +// try { +// const missingInputs: string[] = []; +// // TODO THE CHECK OF THE INPUTS + +// if (missingInputs.length > 0) throw newError('missingInput', '400', missingInputs); + +// return editOneGeneric(model, id, { published: true, publicationDate: new Date() } as any, ctx); +// } catch (error) { +// throw error; +// } +// }; + +// export const unpublishOneGeneric = async (model: T, id: string, ctx: ApolloContext): Promise => { +// try { +// return editOneGeneric(model, id, { published: false } as any, ctx); +// } catch (error) { +// throw error; +// } +// }; diff --git a/lib/seed/engine/utils/streams/schemas/stream.components.ts b/lib/seed/engine/utils/streams/schemas/stream.components.ts new file mode 100644 index 0000000..93843b1 --- /dev/null +++ b/lib/seed/engine/utils/streams/schemas/stream.components.ts @@ -0,0 +1,41 @@ +import { registerEnumType } from 'type-graphql'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +export enum StreamOperationType { + insert = 'insert', + delete = 'delete', + replace = 'replace', + update = 'update', + hook = 'hook', +} +registerEnumType(StreamOperationType, { + name: 'StreamOperationType', +}); + +export enum PostHookStatus { + new = 'new', + inProcess = 'inProcess', + completed = 'completed', + error = 'error', + noAction = 'noAction', +} +registerEnumType(PostHookStatus, { + name: 'PostHookStatus', +}); + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ diff --git a/lib/seed/engine/utils/streams/schemas/stream.schema.input.ts b/lib/seed/engine/utils/streams/schemas/stream.schema.input.ts new file mode 100644 index 0000000..3d76a60 --- /dev/null +++ b/lib/seed/engine/utils/streams/schemas/stream.schema.input.ts @@ -0,0 +1,11 @@ +import { GetManyArgs } from '@seed/graphql/Request'; +import { Field, ArgsType } from 'type-graphql'; + +@ArgsType() +export class StreamArgs extends GetManyArgs { + @Field({ nullable: true }) + documentKey?: string; + + @Field({ nullable: true }) + hookStatus?: string; +} diff --git a/lib/seed/engine/utils/streams/schemas/stream.schema.ts b/lib/seed/engine/utils/streams/schemas/stream.schema.ts new file mode 100644 index 0000000..7637daa --- /dev/null +++ b/lib/seed/engine/utils/streams/schemas/stream.schema.ts @@ -0,0 +1,50 @@ +import { AsyncHooksService } from '@lib/__hooks'; +import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema'; +import { StreamOperationType, PostHookStatus } from '@seed/services/change-stream/change-stream.components'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import GraphQLJSON from 'graphql-type-json'; +import { InputType, ObjectType, Field } from 'type-graphql'; + +@ObjectType() +@InputType() +export class StreamBaseSchema { + @Field(() => StreamOperationType) + operation: StreamOperationType; + + @Field(() => String) + collection: ModelCollectionEnum | keyof AsyncHooksService | string; + + @Field({ nullable: true }) + documentKey?: string; + + @Field(() => PostHookStatus) + hookStatus: PostHookStatus; + + @Field({ nullable: true }) + logMessage?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + insertedValues?: any; + @Field(() => GraphQLJSON, { nullable: true }) + updatedValues?: any; + @Field(() => GraphQLJSON, { nullable: true }) + beforeUpdateValues?: any; + + logInfo?: any; +} + +@ObjectType() +export class StreamDBInterfaceSchema extends StreamBaseSchema {} + +@ObjectType() +export class StreamDBSchema extends StreamDBInterfaceSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class StreamSchema extends StreamDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/lib/seed/engine/utils/streams/stream.model.ts b/lib/seed/engine/utils/streams/stream.model.ts new file mode 100644 index 0000000..e6a1209 --- /dev/null +++ b/lib/seed/engine/utils/streams/stream.model.ts @@ -0,0 +1,49 @@ +import { ObjectType } from 'type-graphql'; + +import { Permission } from '@seed/interfaces/permission'; + +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { StreamDBInterfaceSchema, StreamDBSchema, StreamSchema } from './schemas/stream.schema'; +import { EngineModel } from '@seed/engine/EngineModel'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { plainToClass } from 'class-transformer'; +import { CustomSearchEngine } from '@seed/services/database/DBRequestService'; +import { StreamArgs } from './schemas/stream.schema.input'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class StreamEngineModel extends EngineModel { + public constructor(input?: StreamDBInterfaceSchema & Partial) { + const dataInit = plainToClass(StreamDBSchema, input || {}); + super({ + // ...init, + collectionName: ModelCollectionEnum.streams, + permissions: permissions, + dataInit, + }); + } + + plainToClass(plain: any): StreamSchema | StreamSchema[] { + return plainToClass(StreamSchema, plain); + } + + searchEngine(): CustomSearchEngine { + const sEngine: CustomSearchEngine = { + documentKey: { + operation: '$eq', + }, + hookStatus: { + operation: '$eq', + }, + }; + + return sEngine; + } +} diff --git a/lib/seed/engine/utils/streams/stream.resolver.ts b/lib/seed/engine/utils/streams/stream.resolver.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/seed/engine/utils/streams/stream.service.ts b/lib/seed/engine/utils/streams/stream.service.ts new file mode 100644 index 0000000..0aba53d --- /dev/null +++ b/lib/seed/engine/utils/streams/stream.service.ts @@ -0,0 +1,84 @@ +import { AsyncHooksService } from '@lib/__hooks'; +import { EngineModel } from '@seed/engine/EngineModel'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import DB from '@seed/services/database/DBService'; +import { asyncHook } from '@seed/services/hooks/hooks.decorator'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { v4 as uuid } from 'uuid'; +import { StreamOperationType, PostHookStatus } from './schemas/stream.components'; +import { StreamDBInterfaceSchema } from './schemas/stream.schema'; + +export const createChangeStream = async >(model: T, operation: StreamOperationType): Promise => { + let operationFunction; + + const changeStreamId = uuid(); + const base = { + _id: changeStreamId, + operation, + collection: model.collectionName, + documentKey: model._id, + hookStatus: PostHookStatus.new, + createdAt: new Date(), + updatedAt: new Date(), + }; + + switch (operation) { + case StreamOperationType.insert: + (base as any).insertedValues = model.get(); + operationFunction = 'afterCreate'; + break; + case StreamOperationType.update: + case StreamOperationType.replace: + (base as any).updatedValues = model.get(); + operationFunction = 'afterUpdate'; + break; + case StreamOperationType.delete: + (base as any).updatedValues = model.get(); + operationFunction = 'afterDelete'; + break; + default: + break; + } + + const dataToSave: StreamDBInterfaceSchema & IEngineSchema = { + ...base, + permissions: { + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], + }, + }; + await (await DB.getInstance()).db.collection(ModelCollectionEnum['streams']).insertOne(dataToSave); + + if (operationFunction && model[operationFunction] && (await asyncHook(dataToSave))) { + await model[operationFunction](); + + await (await DB.getInstance()).db + .collection(ModelCollectionEnum['streams']) + .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); + } +}; + +export const createAsyncStream = async >(collection: keyof AsyncHooksService, data: any): Promise => { + const changeStreamId = uuid(); + const base = { + _id: changeStreamId, + operation: StreamOperationType.hook, + collection, + insertedValues: data, + hookStatus: PostHookStatus.new, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const dataToSave: StreamDBInterfaceSchema & IEngineSchema = { + ...base, + permissions: { + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], + }, + }; + await (await DB.getInstance()).db.collection(ModelCollectionEnum['streams']).insertOne(dataToSave); +}; diff --git a/lib/seed/examples/__collections.ts b/lib/seed/examples/__collections.ts new file mode 100644 index 0000000..c455082 --- /dev/null +++ b/lib/seed/examples/__collections.ts @@ -0,0 +1,9 @@ +import { registerEnumType } from 'type-graphql'; + +export enum ModelCollectionEnum { + 'streams' = 'streams', + accounts = 'accounts', +} +registerEnumType(ModelCollectionEnum, { + name: 'ModelLoadersEnum', +}); diff --git a/lib/seed/examples/__cronjobs/serverless-cron.yaml b/lib/seed/examples/__cronjobs/serverless-cron.yaml new file mode 100644 index 0000000..e7bca20 --- /dev/null +++ b/lib/seed/examples/__cronjobs/serverless-cron.yaml @@ -0,0 +1,37 @@ +app: ${file(./package.json):name} +service: cronjob +package: + individually: false +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + AWS_compute: ${self:service}-${self:provider.stage}-compute + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' +plugins: + - serverless-webpack +custom: + stage: ${opt:stage, self:provider.stage} + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk + - puppeteer + +functions: + treatRemainingDaysCron: + handler: lib/__cronjobs/handler.cron1 + timeout: 300 # optional, in seconds, default is 6 + events: + - schedule: rate(1 day) + jobNotificationsCron: + handler: lib/__cronjobs/handler.cron2 + timeout: 300 + events: + - schedule: rate(1 day) diff --git a/lib/seed/examples/__hooks/functions/example.hooks.ts b/lib/seed/examples/__hooks/functions/example.hooks.ts new file mode 100644 index 0000000..fa5d966 --- /dev/null +++ b/lib/seed/examples/__hooks/functions/example.hooks.ts @@ -0,0 +1,8 @@ +import { ApolloContext } from '@seed/interfaces/context'; +import { promiseAll, sleep } from '@seed/helpers/Utils'; + +export const afterCheckoutEvent = async (data: any, ctx: ApolloContext): Promise => { + const promises: Promise[] = []; + + await promiseAll(promises); +}; diff --git a/lib/seed/examples/__hooks/index.ts b/lib/seed/examples/__hooks/index.ts new file mode 100644 index 0000000..e6cfaec --- /dev/null +++ b/lib/seed/examples/__hooks/index.ts @@ -0,0 +1,35 @@ +import { asyncHookDecorator } from '@seed/services/hooks/hooks.decorator'; +import { args } from 'chrome-aws-lambda'; +import { afterCheckoutEvent } from './functions/example.hooks'; +import { afterOrderCancel, afterOrdersMarkOneAsPaid, afterOrdersMarkOneAsUnPaid } from './functions/orders.hooks'; + +@asyncHookDecorator() +export class AsyncHooksService { + /* [MODULES - SHOP] */ + + public async afterCheckout(...args: any): Promise { + console.log('[NO ACTION] - afterCheckout'); + } + + public async afterOrderCancel(...args: Parameters): Promise { + return afterOrderCancel(...args); + } + + public async afterOrdersMarkOneAsPaid(...args: Parameters): Promise { + return afterOrdersMarkOneAsPaid(...args); + } + + public async afterOrdersMarkOneAsUnPaid(...args: Parameters): Promise { + return afterOrdersMarkOneAsUnPaid(...args); + } + + public async afterOrdersReimbursed(...args: Parameters): Promise { + console.log('[NO ACTION] - afterOrdersReimbursed'); + } + + /* [MODULES - EVENTS] */ + + public async afterCheckoutEvent(...args: Parameters): Promise { + return afterCheckoutEvent(...args); + } +} diff --git a/lib/seed/examples/app-rest.ts b/lib/seed/examples/app-rest.ts new file mode 100644 index 0000000..0dd8ea2 --- /dev/null +++ b/lib/seed/examples/app-rest.ts @@ -0,0 +1,11 @@ +import 'reflect-metadata'; + +import serverless from 'serverless-http'; +import express from 'express'; +const app = express(); + +app.get('/', function(req, res) { + res.send('Hello World!'); +}); + +module.exports.handler = serverless(app); diff --git a/lib/seed/examples/config.ts b/lib/seed/examples/config.ts new file mode 100644 index 0000000..bf29051 --- /dev/null +++ b/lib/seed/examples/config.ts @@ -0,0 +1,34 @@ +import ErrorSettingsModel from '@services/module-cms/functions/error-settings/errors-settings.model'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ + +export enum NotificationEnum { + 'notification' = 'notification', +} + +export const ErrorsConfig: { + [key: string]: { + message: string; + translations?: ErrorSettingsModel; + }; +} = { + code: { + message: 'message', + }, +}; diff --git a/lib/seed/examples/domains.yaml b/lib/seed/examples/domains.yaml new file mode 100644 index 0000000..f7eeee6 --- /dev/null +++ b/lib/seed/examples/domains.yaml @@ -0,0 +1,17 @@ +## Instal ## +## run : node ./lib/seed/devops/deploy-domains.js +## Carefull - Must be us-east-1 for certificate (created beforehand) + +domains: + production: XXXX.com + staging: staging-XXXX.com + dev: dev-XXXX.com +customDomain: + basePath: '${self:service}' + domainName: ${self:custom.domains.${self:custom.stage}} + stage: '${self:custom.stage}' + createRoute53Record: true + certificateName: XXXX + certificateArn: arn:aws:acm:us-east-1:555646219416:certificate/07e83105-1554-4e3d-b968-0a34d77911ad + endpointType: 'edge' + securityPolicy: tls_1_2 diff --git a/lib/seed/examples/env/__env.accounts.createAdmin.json b/lib/seed/examples/env/__env.accounts.createAdmin.json new file mode 100644 index 0000000..ce8c293 --- /dev/null +++ b/lib/seed/examples/env/__env.accounts.createAdmin.json @@ -0,0 +1,20 @@ +{ + "_id" : "23wzbPpt4OWXtqs92dAaNvrue9v1", + "d" : [ + "admin" + ], + "email" : "admin@xxxx", + "r" : [ + "admin" + ], + "types" : [ + "admin" + ], + "w" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "firstName" : "Admin", + "lastName" : "Project", +} \ No newline at end of file diff --git a/lib/seed/examples/env/__env.firebase.import.json b/lib/seed/examples/env/__env.firebase.import.json new file mode 100644 index 0000000..ce022e6 --- /dev/null +++ b/lib/seed/examples/env/__env.firebase.import.json @@ -0,0 +1,170 @@ +{ + "_id" : "b92eebfa-2bd9-4976-9a6d-2fba6bad14fb", + "key" : "FIREBASE_project_id", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "f6f8f6d6-25a4-4ce7-a00b-8b0a7188a5cf", + "key" : "FIREBASE_private_key_id", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "00698ef9-7dc3-4942-973d-ac5823ca6399", + "key" : "FIREBASE_private_key", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "829f390c-ca20-4dec-a679-4f2117fee710", + "key" : "FIREBASE_client_email", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "7f86b770-f6ce-4c99-a6ad-7eda6c3eac04", + "key" : "FIREBASE_client_id", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "262c6d6f-ff34-46b4-9639-f8baa75186ce", + "key" : "FIREBASE_auth_uri", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "b34ec29f-02c1-4699-ab08-83dd920bce7a", + "key" : "FIREBASE_token_uri", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "35409f42-f87f-47ae-8505-2328f98a3979", + "key" : "FIREBASE_auth_provider_x509_cert_url", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "fbb3f410-6717-49af-8004-75c9f110e7db", + "key" : "FIREBASE_client_x509_cert_url", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "34e7ad98-3c5b-4288-802e-57f6f67a2c9f", + "key" : "FIREBASE_apiKey", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, \ No newline at end of file diff --git a/lib/seed/examples/env/__env.general.import.json b/lib/seed/examples/env/__env.general.import.json new file mode 100644 index 0000000..5d2c27f --- /dev/null +++ b/lib/seed/examples/env/__env.general.import.json @@ -0,0 +1,169 @@ +{ + "_id" : "28ade57d-697b-4cac-9e50-62cd4a8c8e61", + "key" : "FILE_allowedMime", + "value" :"image/jpeg,image/png,image/gif,application/pdf,image/svg+xml,application/xml,application/zip", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "6f106c8f-df69-403d-b92b-6bba314ce6ef", + "key" : "TEST_USER_EMAIL", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "cfc32155-cd1c-4d5e-bfa9-425ef27065ce", + "key" : "TEST_ADMIN_EMAIL", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "8c4c28fb-d89d-434c-a962-f4c43dbda68d", + "key" : "STRIPE_SKEY", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "dec94533-a457-4c66-9ead-4e754243ec89", + "key" : "STRIPE_PUBLIC_KEY", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "870d1674-367a-4937-9d66-d36e889d4953", + "key" : "STRIPE_CURRENCY", + "value" :"usd", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "5ed19459-1b4b-462b-88fb-5fbb2d698229", + "key" : "STRIPE_ORDERDESCRIPTION", + "value" :"You order from our webplatform", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "354b4491-497f-4e3f-9569-06863e19fadd", + "key" : "MJ_APIKEY_PUBLIC", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "2e69bc94-28c9-400e-bd77-1714fb4f8300", + "key" : "MJ_APIKEY_PRIVATE", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +},{ + "_id" : "5320e684-77af-4432-b229-60d63825a823", + "key" : "GOOGLE_MAP_apiKey", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +} \ No newline at end of file diff --git a/lib/seed/examples/env/__env.mailsettings.import.json b/lib/seed/examples/env/__env.mailsettings.import.json new file mode 100644 index 0000000..8099fa8 --- /dev/null +++ b/lib/seed/examples/env/__env.mailsettings.import.json @@ -0,0 +1,219 @@ +{ + "_id" : "04e48fa2-714f-4d7f-ae17-c791ccf6e5bb", + "key" : "resetPassword", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "You requested a new password", + "fr" : "Vous avez demandé un nouveau mot de passe", + "nl" : "You requested a new password" + }, + "body" : { + "en" : "

Here is the password reset link : ${resetPasswordLink}

", + "fr" : "

Voici le lien pour réinitialiser votre mot de passe : ${resetPasswordLink}

", + "nl" : "

Here is the password reset link : ${resetPasswordLink}

" + }, + "availableFields" : [ + "firstName", + "lastName", + "email", + "resetPasswordLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "0f245c6c-e39a-465f-9374-e2268a0cbf83", + "key" : "adminAccountAddOne", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "The administrator of [PROJECT] created an account for you", + "fr" : "L'administrateur de [PROJECT] a crée un compte pour vous!", + "nl" : "The administrator of [PROJECT] created an account for you" + }, + "body" : { + "en" : "

Please setup your password by clicking this link : ${resetPasswordLink}

", + "fr" : "

Pouvez-vous configurer un mot de passe en cliquant sur ce lien ? : ${resetPasswordLink}

", + "nl" : "

Please setup your password by clicking this link : ${resetPasswordLink}

" + }, + "availableFields" : [ + "firstName", + "lastName", + "email", + "resetPasswordLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "16496dfc-5935-495b-a352-52efe158d55c", + "key" : "organisationsAddOneAccount", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "The administrator of [ORG] created an account for you", + "fr" : "L'administrateur de l'organisation [ORG] a crée un compte pour vous!", + "nl" : "The administrator of [ORG] created an account for you" + }, + "body" : { + "en" : "

Please setup your password by clicking this link : ${resetPasswordLink}

", + "fr" : "

Pouvez-vous configurer un mot de passe en cliquant sur ce lien ? : ${resetPasswordLink}

", + "nl" : "

Please setup your password by clicking this link : ${resetPasswordLink}

" + }, + "availableFields" : [ + "firstName", + "lastName", + "email", + "resetPasswordLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "16496dfc-5875-495b-axx2-52efe158d55c", + "key" : "emailCodeSignIn", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "Here is your code to login : ${code}", + "fr" : "Voici votre code d'accès: ${code}", + "nl" : "Here is your code to login : ${code}" + }, + "body" : { + "en" : "Here is your code to login : ${code}", + "fr" : "Voici votre code d'accès: ${code}", + "nl" : "Here is your code to login : ${code}" + }, + "availableFields" : [ + "firstName", + "code" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "04e48fa2-714f-4d7f-ae17-c791ccf6e5bb", + "key" : "magicLink", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "subject" : { + "en" : "Your magic link is ready", + "fr" : "Votre demande de lien d'accès", + "nl" : "Your magic link is ready" + }, + "body" : { + "en" : "Here is your link to login : ${magicLink}", + "fr" : "Voici votre lien d'accès: ${magicLink}", + "nl" : "Here is your link to login : ${magicLink}" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "availableFields" : [ + "firstName", + "lastName", + "email", + "magicLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +} + diff --git a/lib/seed/examples/env/__env.notifications.import.json b/lib/seed/examples/env/__env.notifications.import.json new file mode 100644 index 0000000..ca30cc4 --- /dev/null +++ b/lib/seed/examples/env/__env.notifications.import.json @@ -0,0 +1,51 @@ +{ + "_id" : "28ade57d-df69-4cac-9e50-62cd4a8c8e61", + "key" : "TWILIO_FROM_NUMBER", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "28ade57d-df69-403d-b92b-6bba314ce6ef", + "key" : "TWILIO_ACCOUNT_SID", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "cfc32155-b92b-4d5e-bfa9-425ef27065ce", + "key" : "TWILIO_AUTH_TOKEN", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, diff --git a/lib/seed/examples/list.import.json b/lib/seed/examples/list.import.json new file mode 100644 index 0000000..ef88703 --- /dev/null +++ b/lib/seed/examples/list.import.json @@ -0,0 +1,47 @@ +mutation listsAddOne { + listsAddOne1: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_2.svg",fr:"combined-shape_2.svg",nl:"combined-shape_2.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_2.svg"}}){_id} + listsAddOne2: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_3.svg",fr:"combined-shape_3.svg",nl:"combined-shape_3.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_3.svg"}}){_id} + listsAddOne3: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_4.svg",fr:"combined-shape_4.svg",nl:"combined-shape_4.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_4.svg"}}){_id} + listsAddOne4: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_5.svg",fr:"combined-shape_5.svg",nl:"combined-shape_5.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_5.svg"}}){_id} + listsAddOne5: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_6.svg",fr:"combined-shape_6.svg",nl:"combined-shape_6.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_6.svg"}}){_id} + listsAddOne6: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_7.svg",fr:"combined-shape_7.svg",nl:"combined-shape_7.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_7.svg"}}){_id} + listsAddOne7: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_8.svg",fr:"combined-shape_8.svg",nl:"combined-shape_8.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_8.svg"}}){_id} + listsAddOne8: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_9.svg",fr:"combined-shape_9.svg",nl:"combined-shape_9.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_9.svg"}}){_id} + listsAddOne9: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_10.svg",fr:"combined-shape_10.svg",nl:"combined-shape_10.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_10.svg"}}){_id} + listsAddOne11: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_11.svg",fr:"combined-shape_11.svg",nl:"combined-shape_11.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_11.svg"}}){_id} + listsAddOne12: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_12.svg",fr:"combined-shape_12.svg",nl:"combined-shape_12.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_12.svg"}}){_id} + listsAddOne13: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_13.svg",fr:"combined-shape_13.svg",nl:"combined-shape_13.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_13.svg"}}){_id} + listsAddOne14: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_14.svg",fr:"combined-shape_14.svg",nl:"combined-shape_14.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_14.svg"}}){_id} + listsAddOne15: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_15.svg",fr:"combined-shape_15.svg",nl:"combined-shape_15.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_15.svg"}}){_id} + listsAddOne16: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_16.svg",fr:"combined-shape_16.svg",nl:"combined-shape_16.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_16.svg"}}){_id} + listsAddOne17: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_17.svg",fr:"combined-shape_17.svg",nl:"combined-shape_17.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_17.svg"}}){_id} + listsAddOne18: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_18.svg",fr:"combined-shape_18.svg",nl:"combined-shape_18.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_18.svg"}}){_id} + listsAddOne19: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_19.svg",fr:"combined-shape_19.svg",nl:"combined-shape_19.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_19.svg"}}){_id} + listsAddOne21: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_20.svg",fr:"combined-shape_20.svg",nl:"combined-shape_20.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_20.svg"}}){_id} + listsAddOne22: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_21.svg",fr:"combined-shape_21.svg",nl:"combined-shape_21.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_21.svg"}}){_id} + listsAddOne23: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_22.svg",fr:"combined-shape_22.svg",nl:"combined-shape_22.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_22.svg"}}){_id} + listsAddOne24: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_23.svg",fr:"combined-shape_23.svg",nl:"combined-shape_23.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_23.svg"}}){_id} + listsAddOne25: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_24.svg",fr:"combined-shape_24.svg",nl:"combined-shape_24.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_24.svg"}}){_id} + listsAddOne26: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_25.svg",fr:"combined-shape_25.svg",nl:"combined-shape_25.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_25.svg"}}){_id} + listsAddOne27: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_26.svg",fr:"combined-shape_26.svg",nl:"combined-shape_26.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_26.svg"}}){_id} + listsAddOne28: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_27.svg",fr:"combined-shape_27.svg",nl:"combined-shape_27.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_27.svg"}}){_id} + listsAddOne29: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_28.svg",fr:"combined-shape_28.svg",nl:"combined-shape_28.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_28.svg"}}){_id} + listsAddOne31: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_29.svg",fr:"combined-shape_29.svg",nl:"combined-shape_29.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_29.svg"}}){_id} + listsAddOne32: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_30.svg",fr:"combined-shape_30.svg",nl:"combined-shape_30.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_30.svg"}}){_id} + listsAddOne33: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_31.svg",fr:"combined-shape_31.svg",nl:"combined-shape_31.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_31.svg"}}){_id} + listsAddOne34: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-2.svg",fr:"combined-shape-copy-2.svg",nl:"combined-shape-copy-2.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-2.svg"}}){_id} + listsAddOne35: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-3.svg",fr:"combined-shape-copy-3.svg",nl:"combined-shape-copy-3.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-3.svg"}}){_id} + listsAddOne36: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-4.svg",fr:"combined-shape-copy-4.svg",nl:"combined-shape-copy-4.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-4.svg"}}){_id} + listsAddOne37: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-5.svg",fr:"combined-shape-copy-5.svg",nl:"combined-shape-copy-5.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-5.svg"}}){_id} + listsAddOne38: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-6.svg",fr:"combined-shape-copy-6.svg",nl:"combined-shape-copy-6.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-6.svg"}}){_id} + listsAddOne39: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-7.svg",fr:"combined-shape-copy-7.svg",nl:"combined-shape-copy-7.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-7.svg"}}){_id} + listsAddOne41: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-8.svg",fr:"combined-shape-copy-8.svg",nl:"combined-shape-copy-8.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-8.svg"}}){_id} + listsAddOne42: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape.svg",fr:"combined-shape.svg",nl:"combined-shape.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape.svg"}}){_id} + listsAddOne43: listsAddOne(input:{ressourceType:feature,title:{en:"group.svg",fr:"group.svg",nl:"group.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/group.svg"}}){_id} + listsAddOne44: listsAddOne(input:{ressourceType:feature,title:{en:"shape_2.svg",fr:"shape_2.svg",nl:"shape_2.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_2.svg"}}){_id} + listsAddOne45: listsAddOne(input:{ressourceType:feature,title:{en:"shape_3.svg",fr:"shape_3.svg",nl:"shape_3.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_3.svg"}}){_id} + listsAddOne46: listsAddOne(input:{ressourceType:feature,title:{en:"shape_4.svg",fr:"shape_4.svg",nl:"shape_4.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_4.svg"}}){_id} + listsAddOne47: listsAddOne(input:{ressourceType:feature,title:{en:"shape_5.svg",fr:"shape_5.svg",nl:"shape_5.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_5.svg"}}){_id} + listsAddOne48: listsAddOne(input:{ressourceType:feature,title:{en:"shape_6.svg",fr:"shape_6.svg",nl:"shape_6.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_6.svg"}}){_id} + listsAddOne49: listsAddOne(input:{ressourceType:feature,title:{en:"shape.svg",fr:"shape.svg",nl:"shape.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape.svg"}}){_id} + } \ No newline at end of file diff --git a/lib/seed/graphql/AccessService.ts b/lib/seed/graphql/AccessService.ts new file mode 100644 index 0000000..e26ced9 --- /dev/null +++ b/lib/seed/graphql/AccessService.ts @@ -0,0 +1,150 @@ +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { newError } from '@seed/helpers/Error'; + +export const checkPermissions = (ressource: any, account: any | null, type: 'c' | 'r' | 'w' | 'd'): boolean => { + // Adding the public by default + let perm: any[] = [AccountTypeEnum.public]; + + if (account && account.types) { + /* + * Verify if admin + */ + if (account.types.includes(AccountTypeEnum.admin)) return true; + + /* + * Verify if organisation type of access + */ + if ( + ressource.organisationId && + account.organisationIds && + !account.organisationIds.includes(ressource.organisationId) && + !ressource.r.includes(AccountTypeEnum.public) + ) + throw newError(2100, { allowed: ressource.organisationId, you: account.organisationIds }); + + /* + * Add the account id and type + */ + perm.push(account._id); + perm = perm.concat(account.types); + } + + /* + * Verify on the ressource level + */ + + const permissions = ressource[type]; + + // if there is no permission on the ressource, return true + if (!permissions) return true; + + let hasPerm = false; + + // Verifying if it matches + for (let index = 0; index < perm.length; index++) { + const element = perm[index]; + if (permissions.includes(element)) { + hasPerm = true; + break; + } + } + + if (!hasPerm) throw newError(2000, { allowed: permissions, you: account }); + + return true; +}; + +export const checkOrganisationPermissions = (ressource: any, organisationId: string): boolean => { + if (organisationId == ressource.organisationId || organisationId == ressource._id) return true; + throw newError(2000, { allowedOrgId: ressource.organisationId, youOrgId: organisationId }); +}; + +export const addPermissions = (ressource: any, type: ('r' | 'w' | 'd')[], ids: (string | string)[]): void => { + for (let index = 0; index < type.length; index++) { + const t = type[index]; + ressource[t] = ressource[t].concat(ids); + } +}; + +export const addPermissionToQuery = (account: any | null, query: 'get' | 'update' | 'delete', params: any): any => { + let types: any[] = ['public']; + + if (account && account.types) { + /* + * Verify if admin, no need to add the query filters + */ + if (account.types.includes(AccountTypeEnum.admin)) return params; + + /* + * Verify if organisation type of access + */ + + // if (account.organisationIds) { + // params.organisationId = { $in: account.organisationIds }; + // } + + /* + * Add the account id and type + */ + + types.push(account._id); + types = types.concat(account.types); + } + + if (!params.$and) params.$and = []; + + switch (query) { + default: + case 'get': + params.$and.push({ r: { $in: types } }); + break; + case 'update': + params.$and.push({ w: { $in: types } }); + break; + case 'delete': + params.$and.push({ d: { $in: types } }); + break; + } + + return params; +}; + +export const addOrganisationToQuery = (account: any | null, query: 'get' | 'update' | 'delete', params: any): any => { + let types: any[] = ['public']; + + if (account && account.types) { + /* + * Verify if admin, no need to add the query filters + */ + if (account.types.includes(AccountTypeEnum.admin)) return params; + + /* + * Verify if organisation type of access + */ + if (account.organisationIds) { + params.organisationId = { $in: account.organisationIds }; + } + + /* + * Add the account id and type + */ + + types.push(account._id); + types = types.concat(account.types); + } + + switch (query) { + default: + case 'get': + params = { ...params, $or: [{ r: { $in: types } }] }; + break; + case 'update': + params = { ...params, $or: [{ w: { $in: types } }] }; + break; + case 'delete': + params = { ...params, $or: [{ d: { $in: types } }] }; + break; + } + + return params; +}; diff --git a/lib/seed/graphql/BaseGraphModel.ts b/lib/seed/graphql/BaseGraphModel.ts new file mode 100644 index 0000000..cb6b704 --- /dev/null +++ b/lib/seed/graphql/BaseGraphModel.ts @@ -0,0 +1,828 @@ +import { v4 as uuid } from 'uuid'; +import { Collection, FilterQuery, UpdateQuery } from 'mongodb'; +import { classToPlain, Exclude, plainToClassFromExist } from 'class-transformer'; +import { Field, ID, ObjectType, Authorized } from 'type-graphql'; +import DB, { createSetRequest } from '@seed/services/database/DBService'; + +import { Permission } from '@seed/interfaces/permission'; +import { newError } from '@seed/helpers/Error'; + +import { parsePaginationOptions } from '@seed/helpers/Request'; +import { GetArgs, GetManyArgs } from './Request'; +import { addPermissions, addPermissionToQuery, checkPermissions, checkOrganisationPermissions } from './AccessService'; +import { ApolloContext } from '@seed/interfaces/context'; +import _ from 'lodash'; +import { StreamOperationType, PostHookStatus } from '@seed/services/change-stream/change-stream.components'; +import { asyncHook } from '@seed/services/hooks/hooks.decorator'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { promiseAll } from '@seed/helpers/Utils'; +import { EnginePathComponent } from '@seed/interfaces/components'; + +export interface BaseInterface { + _id: string; + + updatedAt: Date; + createdAt: Date; + + collectionName: string; +} +@ObjectType() +export abstract class BaseGraphModel implements BaseInterface { + @Exclude() + public collectionName: string; + @Exclude() + protected permissions: Permission; + @Exclude() + defaultSort: string; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + _id: string; + + @Field(() => ID, { nullable: true }) + organisationId?: string; + + @Field(() => ID, { nullable: true }) + createdBy?: string; + + @Field(() => ID, { nullable: true }) + updatedBy?: string; + + @Field(() => ID, { nullable: true }) + deletedBy?: string; + + @Field() + createdAt: Date; + + @Field() + updatedAt: Date; + + @Field(() => [String]) + public r: string[]; + + @Authorized('admin') + @Field(() => [String]) + public w: string[]; + + @Authorized('admin') + @Field(() => [String]) + public d: string[]; + + paths: EnginePathComponent[]; + + public constructor(init: { collectionName: string; permissions: Permission; defaultSort?: string }) { + this.collectionName = init.collectionName; + this.permissions = init.permissions; + + this.r = init.permissions.r; + this.w = init.permissions.w; + this.d = init.permissions.d; + + this.createdAt = new Date(); + this.updatedAt = new Date(); + + this.defaultSort = init.defaultSort || 'createdAt desc'; + } + + /* + * + * Model functions + * + */ + + public get(): any { + return classToPlain(this); + } + + public set(doc: any): void { + plainToClassFromExist(this, doc); + + if (doc._id) this._id = doc._id; + + if (doc.r) this.r = _.uniq(_.concat(this.r, doc.r)); + if (doc.w) this.w = _.uniq(_.concat(this.w, doc.w)); + if (doc.d) this.d = _.uniq(_.concat(this.d, doc.d)); + + if (doc.organisationId) this.organisationId = doc.organisationId; + + if (doc.createdAt) this.createdAt = doc.createdAt; + if (doc.updatedAt) this.updatedAt = doc.updatedAt; + } + + abstract searchOptions(): string[]; + abstract filterOptions(): string[]; + + searchEngine?(): any; + + async prehook?(): Promise; + async beforeCreate?(ctx?: ApolloContext | null): Promise; + async beforeUpdate?(ctx?: ApolloContext | null): Promise; + async beforeDelete?(ctx?: ApolloContext | null): Promise; + + async afterCreate?(changeStream: any): Promise; + async afterUpdate?(changeStream: any): Promise; + async afterDelete?(changeStream: any): Promise; + + /* + * + * DB Functions + * + */ + + public async db(): Promise> { + return await (await DB.getInstance()).db.collection(this.collectionName); + } + + public getPath(): EnginePathComponent[] { + if (this.paths) { + this.paths.push({ + ressourceModel: this.collectionName, + ressourceId: this._id, + }); + + return this.paths; + } else + return [ + { + ressourceModel: this.collectionName, + ressourceId: this._id, + }, + ]; + } + + public getQuery(query: FilterQuery, ctx: ApolloContext | null): any { + let finalQuery: any; + + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + // We add the permission (on r field) to make sure that kind of user has access to the ressource + finalQuery = addPermissionToQuery(account, 'get', { ...query }); + + // We check if it comes from an organisation + // Check of the user's correct authorisation on that organisation comes from the middleware + + if (!ctx.ctx.noOrganisationCheck) { + if (organisationId) { + if (this.collectionName == 'accounts') finalQuery['organisationIds'] = organisationId; + else finalQuery['organisationId'] = { $eq: organisationId }; + } else { + // If admin & public query -> do not add this condition and let search every records) + if ( + account && + account.types && + !account.types.includes(AccountTypeEnum.public) && + !account.types.includes(AccountTypeEnum.admin) + ) { + finalQuery['organisationId'] = { $exists: false }; + } + } + } + } else { + finalQuery = { ...query }; + } + + return finalQuery; + } + + public async getMany(query: FilterQuery, pagination: GetArgs | undefined, ctx: ApolloContext | null): Promise { + const finalQuery = this.getQuery(query, ctx); + + if (!pagination) pagination = { limit: 100, skip: 0 }; + if (!pagination.sort) pagination.sort = this.defaultSort; + const paginationOption = parsePaginationOptions(pagination); + + try { + console.log('search request', JSON.stringify(finalQuery, null, 2)); + return await (await this.db()).find(finalQuery, paginationOption).toArray(); + } catch (error) { + return []; + } + } + + public async getCount(query: FilterQuery, ctx: ApolloContext | null): Promise { + const finalQuery = this.getQuery(query, ctx); + + try { + const result = (await (await this.db()).countDocuments(finalQuery)) as any; + console.log(result); + return result; + } catch (error) { + return 0; + } + } + + public async getAll(query: FilterQuery, ctx: ApolloContext | null): Promise { + const finalQuery = this.getQuery(query, ctx); + try { + return await (await this.db()).find(finalQuery).toArray(); + } catch (error) { + return []; + } + } + + public async getOne(query: FilterQuery, ctx: ApolloContext | null): Promise { + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + const doc = await (await this.db()).findOne(query); + if (!doc) throw newError(404, query); + + this.set(doc); + + if (!ctx.ctx.noOrganisationCheck) { + if (organisationId) { + if (this.collectionName != 'accounts') checkOrganisationPermissions(this, organisationId); + } + } + + checkPermissions(this, account, 'r'); + + return this; + } else { + const doc = await (await this.db()).findOne(query); + if (!doc) throw newError(404, query); + + this.set(doc); + return this; + } + } + + public async saveOne(newData: Partial, ctx: ApolloContext | null, upsert = true): Promise { + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + addPermissions(this, ['r', 'w', 'd'], [account._id]); + + if (organisationId) { + newData.organisationId = organisationId; + } + + newData.createdBy = account._id; + } + + this.set(newData); + + // if (!ctx) ctx = await createApolloContext(); + + try { + if (this.prehook) await this.prehook(); + if (this.beforeCreate) await this.beforeCreate(ctx); + } catch (error) { + throw error; + } + + const { _id, ...dataToSave } = this.get(); + const savedId = _id ? _id : uuid(); + + try { + const result = await (await this.db()).findOneAndUpdate( + { _id: savedId }, + { + $set: dataToSave, + $setOnInsert: { + _id: savedId, + }, + } as any, + { upsert, returnOriginal: false }, + ); + + if (result.ok == 0) throw newError(1000, result); + + this.set({ _id: savedId }); + + const changeStreamId = uuid(); + const changeStreamData = { + _id: changeStreamId, + operation: StreamOperationType.insert, + collection: this.collectionName, + documentKey: this._id, + insertedValues: this.get(), + hookStatus: PostHookStatus.new, + createdAt: new Date(), + updateAt: new Date(), + }; + await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData); + + if (this.afterCreate && (await asyncHook(changeStreamData))) { + await this.afterCreate(changeStreamData); + + await (await DB.getInstance()).db + .collection('stream.changes') + .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); + } + + return this; + } catch (error) { + throw newError(1000, error); + } + } + + public async saveMany(newDataArr: Partial[], ctx: ApolloContext | null): Promise { + // Initialize the change stream + const changeStreams: any[] = []; + const afterCreatePromises: any[] = []; + + const prehooksPromises = newDataArr.map(async (newData) => { + const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this); + if (ctx) { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + addPermissions(clone, ['r', 'w', 'd'], [account._id]); + + if (organisationId) { + newData.organisationId = organisationId; + } + + newData.createdBy = account._id; + } + + clone.set(newData); + + // if (!ctx) ctx = await createApolloContext(); + + if (clone.prehook) await clone.prehook(); + if (clone.beforeCreate) await clone.beforeCreate(ctx); + + const { _id, ...dataToSave } = clone.get(); + const savedId = _id ? _id : uuid(); + clone.set({ _id: savedId }); + + const changeStreamId = uuid(); + const changeStreamData = { + _id: changeStreamId, + operation: StreamOperationType.insert, + collection: clone.collectionName, + documentKey: clone._id, + insertedValues: clone.get(), + hookStatus: PostHookStatus.new, + createdAt: new Date(), + updateAt: new Date(), + }; + + changeStreams.push(changeStreamData); + + // eslint-disable-next-line @typescript-eslint/no-this-alias + afterCreatePromises.push(async () => { + if (clone.afterCreate) { + const res = await asyncHook(changeStreamData); + + if (res) { + await clone.afterCreate(changeStreamData); + + await (await DB.getInstance()).db + .collection('stream.changes') + .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); + } + } + }); + + return clone.get(); + }); + + try { + const dataArr = await Promise.all(prehooksPromises); + await (await this.db()).insertMany(dataArr, { ordered: false }); + await (await DB.getInstance()).db.collection('stream.changes').insertMany(changeStreams, { ordered: false }); + await promiseAll(afterCreatePromises); + return dataArr; + } catch (error) { + console.error(error); + throw newError(1000, error); + } + } + + public async updateOne(query: FilterQuery, newData: Partial, ctx: ApolloContext | null): Promise { + try { + const dataThatHaveBeenChanged = createSetRequest('', { ...newData, updatedAt: new Date() }); + let dataBeforeChange: any; + + if (ctx) { + const account = ctx.ctx.user; + // Check if doc exists & correct permissions + if (!this._id) await this.getOne(query, ctx); + + dataBeforeChange = this.get(); + + dataThatHaveBeenChanged.updatedBy = account._id; + + this.set(dataThatHaveBeenChanged); + if (this.beforeUpdate) await this.beforeUpdate(ctx); + + checkPermissions(this, account, 'w'); + + const result = await (await this.db()).findOneAndUpdate( + query, + { $set: dataThatHaveBeenChanged }, + { upsert: false, returnOriginal: false }, + ); + if (result.ok != 1) throw newError(1000, result); + + this.set(result.value); + } else { + if (!this._id) await this.getOne(query, null); + dataBeforeChange = this.get(); + + this.set(dataThatHaveBeenChanged); + if (this.beforeUpdate) await this.beforeUpdate(); + + const result = await (await this.db()).findOneAndUpdate( + query, + { $set: dataThatHaveBeenChanged }, + { upsert: false, returnOriginal: false }, + ); + if (result.ok != 1) throw newError(1000, result); + this.set(result.value); + } + + const changeStreamId = uuid(); + const changeStreamData = { + _id: changeStreamId, + operation: StreamOperationType.update, + collection: this.collectionName, + documentKey: this._id, + updatedValues: dataThatHaveBeenChanged, + beforeUpdateValues: dataBeforeChange, + hookStatus: PostHookStatus.new, + createdAt: new Date(), + updateAt: new Date(), + }; + await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData); + + if (this.afterUpdate && (await asyncHook(changeStreamData))) { + await this.afterUpdate(changeStreamData); + + await (await DB.getInstance()).db + .collection('stream.changes') + .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); + } + + return this; + } catch (error) { + throw error; + } + } + + public async updateOneCustom(query: FilterQuery, set: UpdateQuery, ctx: ApolloContext | null): Promise { + try { + let dataBeforeChange: any; + + if (ctx) { + const account = ctx.ctx.user; + // Check if doc exists + if (!this._id) await this.getOne(query, ctx); + dataBeforeChange = this.get(); + + checkPermissions(this, account, 'w'); + + // Validate new data that will go into the DB + // const validation = await this.validate(); + // if (!validation.success) return validation; + + // Prepare data + this.updatedAt = new Date(); + + const result = await (await this.db()).findOneAndUpdate(query, set, { upsert: false, returnOriginal: false }); + if (result.ok != 1) throw newError(1000, result); + + if (result.value) this.set(result.value); + } else { + if (!this._id) await this.getOne(query, null); + dataBeforeChange = this.get(); + + // Prepare data + this.updatedAt = new Date(); + + const result = await (await this.db()).findOneAndUpdate(query, set, { upsert: false, returnOriginal: false }); + if (result.ok != 1) throw newError(1000, result); + + if (result.value) this.set(result.value); + } + + const changeStreamId = uuid(); + const changeStreamData = { + _id: changeStreamId, + operation: StreamOperationType.update, + collection: this.collectionName, + documentKey: this._id, + hookStatus: PostHookStatus.new, + updatedValues: JSON.stringify(set), + beforeUpdateValues: dataBeforeChange, + createdAt: new Date(), + updateAt: new Date(), + }; + await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData); + if (this.afterUpdate && (await asyncHook(changeStreamData))) { + await this.afterUpdate(changeStreamData); + + await (await DB.getInstance()).db + .collection('stream.changes') + .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); + } + + return this; + } catch (error) { + throw error; + } + } + + public async deleteOne(query: FilterQuery, ctx: ApolloContext | null): Promise { + try { + if (ctx) { + const account = ctx.ctx.user; + if (!this._id) await this.getOne(query, ctx); + checkPermissions(this, account, 'd'); + + if (this.beforeDelete) await this.beforeDelete(ctx); + + const result = await (await this.db()).findOneAndDelete(query); + + if (result.ok != 1) throw newError(1001); + + const deletedModel = '_deleted_' + this.collectionName; + await (await DB.getInstance()).db.collection(deletedModel).insertOne({ ...result.value, deletedBy: account._id }); + } else { + if (!this._id) await this.getOne(query, null); + + if (this.beforeDelete) await this.beforeDelete(); + + const result = await (await this.db()).findOneAndDelete(query); + + if (result.ok != 1) throw newError(1001); + + const deletedModel = '_deleted_' + this.collectionName; + await (await DB.getInstance()).db.collection(deletedModel).insertOne(result.value); + } + const changeStreamId = uuid(); + + const changeStreamData = { + _id: changeStreamId, + operation: StreamOperationType.delete, + collection: this.collectionName, + documentKey: this._id, + hookStatus: PostHookStatus.new, + beforeUpdateValues: this.get(), + createdAt: new Date(), + updateAt: new Date(), + }; + await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData); + + if (this.afterDelete && (await asyncHook(changeStreamData))) { + await this.afterDelete(changeStreamData); + + await (await DB.getInstance()).db + .collection('stream.changes') + .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } }); + } + + return this; + } catch (error) { + throw error; + } + } + + /* + * + * Permissions + * + */ + + public async updateQueryWithPermission(query: FilterQuery, ctx: ApolloContext): Promise> { + const account = ctx.ctx.user; + const organisationId = ctx.ctx.organisationId; + + // We add the permission (on r field) to make sure that kind of user has access to the ressource + const finalQuery = addPermissionToQuery(account, 'get', { ...query }); + + // We check if it comes from an organisation + // Check of the user's correct authorisation on that organisation comes from the middleware + if (organisationId) { + finalQuery['organisationId'] = { $eq: organisationId }; + } else { + finalQuery['organisationId'] = { $exists: false }; + } + + return finalQuery; + } + + // public async updateOnePermission( + // query: FilterQuery, + // add: 'add' | 'remove', + // type: 'r' | 'w' | 'd', + // newPerm: string, + // account?: AccountModel, + // ): Promise { + // // Get the object from the DB + // try { + // // Check if doc exists + // // TODO : if (this.get() returns, then don't) + // await this.getOne(query, account); + // if (account) this.checkPermissions(account.get(), 'w'); + + // // Validate new data that will go into the DB + // // const validation = await this.validate(); + // // if (!validation.success) return validation; + + // // Prepare data + // this.updatedAt = new Date(); + + // if (add == 'add') { + // const result = await (await this.db()).findOneAndUpdate( + // query, + // { $addToSet: { [type]: newPerm } }, + // { upsert: false, returnOriginal: false }, + // ); + // if (result.ok != 1) throw newError('general.dbError', '400', result); + + // this.set(result.value); + // } else if (add == 'remove') { + // const result = await (await this.db()).findOneAndUpdate( + // query, + // { $pull: { [type]: newPerm } }, + // { upsert: false, returnOriginal: false }, + // ); + // if (result.ok != 1) throw newError('general.dbError', '400', result); + + // this.set(result.value); + // } + + // return this; + // } catch (error) { + // throw error; + // } + // } + + public async addOneSubModel( + parentQ: { _id: string }, + subModelRefField: string, + subModelFieldName: keyof this, + subModel: SUB, + subModelInput: any, + ctx: ApolloContext | null, + ): Promise { + try { + subModelInput[subModelRefField] = parentQ._id; + const subM = await subModel.saveOne(subModelInput, ctx); + await this.updateOneCustom(parentQ as any, { $push: { [`${subModelFieldName}`]: subM._id } } as any, null); + return subM; + } catch (error) { + throw error; + } + } + + public async editOneSubModel( + subModel: SUB, + subModelId: string, + subModelInput: any, + ctx: ApolloContext | null, + ): Promise { + try { + // No need to check if belongs to the correct organisation, the middleware does that + const subM = await subModel.updateOne({ _id: subModelId } as any, subModelInput, ctx); + return subM; + } catch (error) { + throw error; + } + } + + public async deleteOneSubModel( + parentQ: FilterQuery, + subModelFieldName: keyof this, + subModel: SUB, + subModelId: string, + ctx: ApolloContext | null, + ): Promise { + try { + const subM = await subModel.deleteOne({ _id: subModelId } as any, ctx); + await this.updateOneCustom(parentQ, { $pull: { [`${subModelFieldName}`]: subM._id } } as any, null); + return subM; + } catch (error) { + throw error; + } + } + + public getOneSubRessource(subRessoureFieldName: keyof this | string, subRessoureId: string): any | undefined { + try { + const subress = _.get(this, subRessoureFieldName); + if (subress) { + const ind = _.findIndex(subress, { _id: subRessoureId }); + if (ind !== -1) return subress[ind]; + } + return; + } catch (error) { + throw error; + } + } + + public getDefaultSubRessource(subRessoureFieldName: keyof this | string): any | undefined { + try { + const subress = _.get(this, subRessoureFieldName); + if (subress) { + const ind = _.findIndex(subress, { default: true }); + if (ind !== -1) return subress[ind]; + return subress[0]; + } + return; + } catch (error) { + throw error; + } + } + + public async addOneSubRessource( + parentQ: { _id: string }, + subRessoureFieldName: keyof this | string, + subRessoure: any, + ctx: ApolloContext | null, + ): Promise { + try { + if (subRessoure.default && _.get(this, subRessoureFieldName)) { + // Remove all default from others + await this.updateOneCustom( + { _id: this._id } as any, + { + $set: { [`${subRessoureFieldName}.$[].default`]: false } as any, + }, + ctx, + ); + } else subRessoure.default = false; + + // Generate ID if not in the model + if (!subRessoure._id) subRessoure._id = uuid(); + await this.updateOneCustom(parentQ as any, { $push: { [`${subRessoureFieldName}`]: subRessoure } } as any, ctx); + return subRessoure; + } catch (error) { + throw error; + } + } + + public async editOneSubRessource( + parentQ: { _id: string }, + subRessoureFieldName: keyof this | string, + subRessoureId: string, + subRessoure: any, + ctx: ApolloContext | null, + ): Promise { + try { + if (!this.getOneSubRessource(subRessoureFieldName, subRessoureId)) throw newError(404); + + if (subRessoure.default) { + // Remove all default from others + await this.updateOneCustom( + { _id: this._id } as any, + { + $set: { [`${subRessoureFieldName}.$[].default`]: false } as any, + }, + ctx, + ); + } else subRessoure.default = false; + + const filterQ = { + ...parentQ, + [`${subRessoureFieldName}._id`]: subRessoureId, + }; + await this.updateOneCustom( + filterQ as any, + { + $set: { + [`${subRessoureFieldName}.$`]: { + ...subRessoure, + _id: subRessoureId, + }, + } as any, + }, + ctx, + ); + return { + ...subRessoure, + _id: subRessoureId, + }; + } catch (error) { + throw error; + } + } + + public async deleteOneSubRessource( + parentQ: { _id: string }, + subRessoureFieldName: keyof this | string, + subRessoureId: string, + ctx: ApolloContext | null, + ): Promise { + try { + if (!this.getOneSubRessource(subRessoureFieldName, subRessoureId)) throw newError(404); + + const filterQ = { + ...parentQ, + [`${subRessoureFieldName}._id`]: subRessoureId, + }; + + await this.updateOneCustom(filterQ as any, { $pull: { [`${subRessoureFieldName}`]: { _id: subRessoureId } } } as any, ctx); + return subRessoureId; + } catch (error) { + throw error; + } + } +} diff --git a/lib/seed/graphql/BaseService.ts b/lib/seed/graphql/BaseService.ts new file mode 100644 index 0000000..5ac8931 --- /dev/null +++ b/lib/seed/graphql/BaseService.ts @@ -0,0 +1,122 @@ +import _ from 'lodash'; + +import { ApolloError } from 'apollo-server-lambda'; + +import { GetArgs } from './Request'; +import { ApolloContext } from '@seed/interfaces/context'; +import { BaseGraphModel } from './BaseGraphModel'; +import { NewPermissionInput } from '@seed/interfaces/components'; +import { baseSearchFunction } from '@seed/services/database/DBRequestService'; +import { newError } from '@seed/helpers/Error'; + +export const getOneGeneric = async (model: T, id: string, ctx: ApolloContext): Promise => { + try { + await model.getOne({ _id: id } as any, ctx); + return model.get(); + } catch (error) { + throw error; + } +}; + +export const addOneGeneric = async (model: T, body: Partial, ctx: ApolloContext): Promise => { + try { + await model.saveOne(body, ctx); + return model.get(); + } catch (error) { + throw error; + } +}; + +export const editOneGeneric = async (model: T, id: string, body: Partial, ctx: ApolloContext): Promise => { + try { + await model.updateOne({ _id: id } as any, body, ctx); + return model.get(); + } catch (error) { + throw error; + } +}; + +export const deleteOneGeneric = async (model: T, id: string, ctx: ApolloContext): Promise => { + try { + await model.deleteOne({ _id: id } as any, ctx); + return model.get(); + } catch (error) { + throw error; + } +}; + +export const getManyGeneric = async (model: T, query: any, pagination: GetArgs, ctx: ApolloContext): Promise => { + return baseSearchFunction({ + model: model, + query: query, + pagination: pagination, + ctx: ctx, + }); +}; + +export const getCountGeneric = async (model: T, baseArguments: any, ctx: ApolloContext): Promise => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { pagination, ...args } = baseArguments; + + const result = await baseSearchFunction({ + model: model, + query: args, + count: true, + ctx: ctx, + }); + + return result.count || 0; +}; + +export const getManyGenericWithArgs = async (model: T, baseArguments: any, ctx: ApolloContext): Promise => { + try { + const { pagination, ...args } = baseArguments; + return baseSearchFunction({ + model: model, + query: args, + pagination: pagination, + ctx: ctx, + }); + } catch (error) { + throw error; + } +}; + +export const permissionsAddOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise => { + try { + await model.updateOneCustom({ _id: id }, { $addToSet: { [`${input.permissionType}`]: input.permission } }, ctx); + return model.get(); + } catch (error) { + throw error; + } +}; + +export const permissionsRemoveOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise => { + try { + await model.updateOneCustom({ _id: id }, { $pull: { [`${input.permissionType}`]: input.permission } }, ctx); + return model.get(); + } catch (error) { + throw error; + } +}; + +export const publishOneGeneric = async (model: T, id: string, ctx: ApolloContext): Promise => { + try { + const missingInputs: string[] = []; + // TODO THE CHECK OF THE INPUTS + + if (missingInputs.length > 0) throw newError(3000, missingInputs); + + return editOneGeneric(model, id, { published: true, publicationDate: new Date() } as any, ctx); + } catch (error) { + throw error; + } +}; + +export const unpublishOneGeneric = async (model: T, id: string, ctx: ApolloContext): Promise => { + try { + return editOneGeneric(model, id, { published: false } as any, ctx); + } catch (error) { + throw error; + } +}; diff --git a/lib/seed/graphql/Middleware.ts b/lib/seed/graphql/Middleware.ts new file mode 100644 index 0000000..de4919e --- /dev/null +++ b/lib/seed/graphql/Middleware.ts @@ -0,0 +1,204 @@ +import _ from 'lodash'; +import { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import { ApolloError } from 'apollo-server-lambda'; +import { AuthChecker, createMethodDecorator } from 'type-graphql'; + +import Firebase from '@seed/services/auth/FirebaseService'; +import { ApolloContext } from '@seed/interfaces/context'; + +import AccountModel from 'src/accounts/account.model'; +import Loaders from '@src/__indexes/__loaders'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +import { simpleEstimator, getComplexity, fieldExtensionsEstimator } from 'graphql-query-complexity'; +import { clog } from '@seed/helpers/Utils'; +import { newError } from '@seed/helpers/Error'; +import { SecurityLevelEnum } from '@services/module-accounts/account.components'; + +export const ctxMiddleware = async (ctx: { event?: APIGatewayProxyEvent; context?: Context; req?: any }): Promise => { + const user: AccountModel = new AccountModel(); + let headers: any = {}; + let organisationId = null; + + try { + if (ctx.event) headers = ctx.event.headers; + else if (ctx.req) headers = ctx.req.headers; + + /* + * Deal with Auth + */ + + const authorization: string = headers['x-authorization'] || headers.authorization || headers.Authorization; + + if (authorization) { + // Verify the token + const firebaseRes = await Firebase.getInstance().tokenIdentify(authorization); + // Verify that exists in DB + await user.getOne({ _id: firebaseRes.uid }, null); + + /* + * Deal with Security + */ + + const securityCheck = user.securityCheck(); + if (!securityCheck) { + throw newError(2400, { + security: user.security, + securityLevel: process.env.SECURITY_LEVEL || SecurityLevelEnum.nothing, + }); + } + + /* + * Deal with Organisation + */ + organisationId = headers.organisationId || headers.organisationid; + if (organisationId) { + // Check if admin + if (user.types && user.types.includes(AccountTypeEnum.admin)) { + console.log('[ADMIN BYPASS]'); + } else if (!user._id || !user.organisationIds || !user.organisationIds?.includes(organisationId)) + throw newError(2101, { + accountOrganisationIds: user.organisationIds, + organisationId: organisationId, + }); + + console.log('[ORG]', organisationId); + } + + console.log('[AUTH - ]', user.types, user?._id); + } else { + console.log('no auth'); + } + } catch (error) { + throw error; + } + + return { + event: ctx.event || ctx.req || {}, + context: ctx.context || ctx.req || {}, + ctx: { + organisationId: organisationId, + user: user, + loaders: new Loaders(), + }, + }; +}; + +export const authMiddleware: AuthChecker = ({ root, args, context, info }, roles) => { + const user = context.ctx.user; + + if (!user || !user._id) { + throw newError(2004); + } + + if (roles.length > 0) { + roles.push(AccountTypeEnum.admin); + + const intersection = _.intersection(roles, user.types); + if (intersection.length > 0) return true; + + // no roles matched, restrict access + throw newError(2000, { you: user.types, allowed: roles }); + } + + return true; +}; + +export const checkOrganisation = (check = false): any => { + return createMethodDecorator(async ({ root, args, context, info }, next) => { + if (check && !(context as any).ctx.organisationId) throw newError(2102, '400'); + return next(); + }); +}; + +export const totalPublic = (check = false): any => { + return createMethodDecorator(async ({ root, args, context, info }, next) => { + if (check) (context as any).ctx.noOrganisationCheck = true; + return next(); + }); +}; + +export const errorMiddleware = (err): any => { + // Don't give the specific errors to the client. + + if (err.extensions) { + const { translations, ...data } = err.extensions; + const errorToReturn = { + code: err.extensions.code, + message: err.extensions.message || err.message, + translations, + data: process.env.NODE_ENV == 'production' ? { message: 'see logs' } : data, + }; + clog(errorToReturn); + + return errorToReturn; + } + + // Otherwise return the original error. The error can also + // be manipulated in other ways, so long as it's returned. + return err; +}; + +export const complexityMiddleware = (schema: any, variables: any, query: any) => { + const maximumComplexity = process.env.GRAPH_COMPLEXITY ? parseInt(process.env.GRAPH_COMPLEXITY) : 1000; + + const complexity = getComplexity({ + schema: schema, + + // The maximum allowed query complexity, queries above this threshold will be rejected + + // The query variables. This is needed because the variables are not available + // in the visitor of the graphql-js library + variables: variables, + + // specify operation name only when pass multi-operation documents + query: query, + + // Add any number of estimators. The estimators are invoked in order, the first + // numeric value that is being returned by an estimator is used as the field complexity. + // If no estimator returns a value, an exception is raised. + estimators: [ + // Add more estimators here... + fieldExtensionsEstimator(), + // This will assign each field a complexity of 1 if no other estimator + // returned a value. + simpleEstimator({ + defaultComplexity: 1, + }), + ], + }); + + // Here we can react to the calculated complexity, + // like compare it with max and throw error when the threshold is reached. + if (complexity >= maximumComplexity) { + throw new Error(`Sorry, too complicated query! ${complexity} is over ${maximumComplexity} that is the max allowed complexity.`); + } + // And here we can e.g. subtract the complexity point from hourly API calls limit. + console.log('Used query complexity points:', complexity); +}; + +export const oneToManyComplexity = ({ args, childComplexity }): number => { + if (args.pagination && args.pagination.limit) return childComplexity * args.pagination.limit; + else return childComplexity * 10; +}; + +export const oneToManyComplexityNoLoader = ({ args, childComplexity }): number => { + if (args.pagination && args.pagination.limit) return childComplexity * args.pagination.limit * 100; + else return childComplexity * 10 * 100; +}; + +export const createApolloContext = async (accountId?: string, organisationId: string | null = null): Promise => { + const user: AccountModel = new AccountModel(); + if (accountId) { + await user.getOne({ _id: accountId }, null); + } + return { + event: {} as any, + context: {} as any, + ctx: { + organisationId, + user: user, + loaders: new Loaders(), + }, + }; +}; diff --git a/lib/seed/graphql/MiddlewareV2.ts b/lib/seed/graphql/MiddlewareV2.ts new file mode 100644 index 0000000..e0aae0e --- /dev/null +++ b/lib/seed/graphql/MiddlewareV2.ts @@ -0,0 +1,132 @@ +import _ from 'lodash'; +import { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import { ApolloError } from 'apollo-server-lambda'; +import { ClassType, createMethodDecorator, MiddlewareFn } from 'type-graphql'; + +import Firebase from '@seed/services/auth/FirebaseService'; +import { ApolloContext } from '@seed/interfaces/context'; + +import AccountModel from 'src/accounts/account.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { validateOrReject } from 'class-validator'; +import { plainToClass } from 'class-transformer'; +import { newError } from '@seed/helpers/Error'; + +export interface EngineMiddlewareInput { + authorization?: AccountTypeEnum[]; + apiKey?: boolean; + checkOrganisation?: boolean; + noOrganisationCheck?: boolean; + noPermissionCheck?: boolean; + validations?: { + inputName?: string; + schema: ClassType; + }[]; +} + +export const checkApiKey = async ({ context }): Promise => { + const user: AccountModel = new AccountModel(); + let headers: any = {}; + if (context.event) headers = context.event.headers; + else if (context.req) headers = context.req.headers; + + /* + * Deal with Auth + */ + + const apiKey: string = headers['x-api-key']; + + if (apiKey) { + // Verify that exists in DB + try { + const account = await user.getOne({ 'apiKeys.key': apiKey }, null); + context.ctx.user = account; + return true; + } catch (error) { + throw newError(2200, '403'); + } + } else throw newError(2202, { required: 'X-API-Key' }); +}; + +export const checkAuth = ({ context }, user, roles: string[]): boolean => { + let headers: any = {}; + if (context.event) headers = context.event.headers; + else if (context.req) headers = context.req.headers; + + if (roles.includes(AccountTypeEnum.public)) return true; + + const authorization: string = headers['x-authorization'] || headers.authorization || headers.Authorization; + if (!authorization) { + throw newError(2002); + } + + if (!user || !user._id) { + throw newError(2004, '403'); + } + + if (roles.length > 0) { + roles.push(AccountTypeEnum.admin); + user.types.push(AccountTypeEnum.public); + + const rolesToVerif = _.uniq(roles); + const userTypes = _.uniq(user.types); + + const intersection = _.intersection(rolesToVerif, userTypes); + if (intersection.length > 0) return true; + + // no roles matched, restrict access + throw newError(2000, { you: user.types, allowed: roles }); + } + + return true; +}; + +export const EngineMiddleware = (init: EngineMiddlewareInput): any => { + return createMethodDecorator(async (data, next) => { + // console.log('engine', data); + const { context, args } = data; + let returnError; + + if (init.authorization) { + const user = (context as any).ctx.user; + try { + checkAuth({ context }, user, init.authorization); + } catch (error) { + returnError = error; + } + } + if (init.apiKey) { + try { + await checkApiKey({ context }); + returnError = null; + } catch (error) { + returnError = error; + } + } + + if (returnError) throw returnError; + + if (init.checkOrganisation && !(context as any).ctx.organisationId) throw newError(2102, '400'); + if (init.noOrganisationCheck) (context as any).ctx.noOrganisationCheck = true; + if (init.noPermissionCheck) (context as any).ctx.noPermissionCheck = true; + + if (init.validations) + for (let index = 0; index < init.validations.length; index++) { + const element = init.validations[index]; + if (!element.inputName) element.inputName = 'input'; + try { + const instance = plainToClass(element.schema, args[element.inputName]); + await validateOrReject(instance); + } catch (error) { + throw newError(3001, error); + } + } + + return next(); + }); +}; + +export const inputValidation: MiddlewareFn = async ({ root, args, context, info }, next) => { + console.log({ root, args, context, info }); + return next(); +}; diff --git a/lib/seed/graphql/Request.ts b/lib/seed/graphql/Request.ts new file mode 100644 index 0000000..f2a6bec --- /dev/null +++ b/lib/seed/graphql/Request.ts @@ -0,0 +1,54 @@ +import { Field, Int, InputType, ArgsType } from 'type-graphql'; +import { Min, Max } from 'class-validator'; +import { DateTabType, RangeType } from '@seed/interfaces/components'; +import { DateRangeComponent } from '@seed/interfaces/components.dates'; + +@InputType() +export class GetArgs { + @Field(() => Int) + @Min(1) + @Max(100) + limit: number; + + @Field(() => Int) + @Min(0) + skip: number; + + @Field({ nullable: true }) + sort?: string; +} + +@ArgsType() +export class GetManyArgs { + @Field(() => [String], { nullable: true }) + _ids?: string[]; + + @Field(() => String, { nullable: true }) + search?: string; + + @Field(() => Date, { nullable: true }) + afterCreatedAt?: Date; + @Field(() => Date, { nullable: true }) + afterUpdatedAt?: Date; + + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@ArgsType() +export class GetManyArgsWithDateRange extends GetManyArgs { + @Field({ nullable: true }) + startDate?: Date; + + @Field({ nullable: true }) + endDate?: Date; + + @Field(() => RangeType, { nullable: true }) + rangeType?: RangeType; + + @Field(() => DateTabType, { nullable: true }) + dateTabType?: DateTabType; + + @Field(() => DateRangeComponent, { nullable: true }) + dateRange?: DateRangeComponent; +} diff --git a/lib/seed/graphql/Server.ts b/lib/seed/graphql/Server.ts new file mode 100644 index 0000000..a0a4946 --- /dev/null +++ b/lib/seed/graphql/Server.ts @@ -0,0 +1,102 @@ +import { nullToUndefined } from '@seed/helpers/Utils'; +import { ApolloServer } from 'apollo-server-lambda'; +import { buildSchema } from 'type-graphql'; +import { ctxMiddleware, authMiddleware, errorMiddleware, complexityMiddleware } from './Middleware'; + +import { SettingsCache } from './Settings'; + +export async function createServer(resolversEntry: any): Promise { + const schema = await buildSchema({ + resolvers: resolversEntry, + authChecker: authMiddleware, + validate: false, + }); + + const server = new ApolloServer({ + // typeDefs, + // resolvers, + schema, + formatError: errorMiddleware, + formatResponse: (response): any => { + return nullToUndefined(response); + }, + context: ctxMiddleware, + tracing: true, + plugins: [ + { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + requestDidStart: () => ({ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + didResolveOperation({ request, document }) { + return complexityMiddleware(schema, request.variables, document); + }, + }), + }, + ], + }); + return server; +} + +export class ServerInstance { + private static instance: ServerInstance; + public handler!: any; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + public static async getInstance(resolversEntry?: any): Promise { + const settingsI = SettingsCache.getInstance(); + await settingsI.refreshCache(); + + if (!this.instance) { + const instance = new ServerInstance(); + + try { + // let { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ + // resolvers: resolversEntry, + // authChecker: authMiddleware, + // }); + + // typeDefs = typeDefs.replace('scalar Upload', 'scalar UploadFix'); + // resolvers = resolvers; + + const server = await createServer(resolversEntry); + instance.handler = server.createHandler({ + cors: { + origin: '*', + credentials: true, + allowedHeaders: 'Content-Type,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,organisationid', + }, + }); + console.log(`[GraphQL] Using new server in ${process.env.NODE_ENV}`); + this.instance = instance; + } catch (error) { + console.log('[GRAPH ERROR]', error); + throw error; + } + } else { + console.log(`[GraphQL] Using same server in ${process.env.NODE_ENV}`); + } + + return this.instance; + } + + public run(event: AWSLambda.APIGatewayProxyEvent, context: AWSLambda.Context): Promise { + try { + console.log('BEGIN ACTION', event.body ? JSON.parse(event.body) : 'NO BODY'); + } catch (error) { + console.log('BEGIN ACTION - Not JSON'); + } + return new Promise((resolve, reject) => { + const callback = (error: any, body: unknown): void => { + console.log('END ACTION'); + if (error) { + console.error(error); + reject(error); + } else resolve(body); + }; + + this.handler(event, context, callback); + }); + } +} diff --git a/lib/seed/graphql/Settings.ts b/lib/seed/graphql/Settings.ts new file mode 100644 index 0000000..538de69 --- /dev/null +++ b/lib/seed/graphql/Settings.ts @@ -0,0 +1,202 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable prettier/prettier */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import { DateTime } from 'luxon'; +import _ from 'lodash'; + +import env from '@config/.env.json'; +import packageFile from 'package.json'; + +import { SettingsType } from '@seed/interfaces/components'; + +import { ErrorsConfig, NotificationEnum } from '@config/config'; +import { EngineNotificationEnum } from '@seed/interfaces/components.notifications'; +import { TranslatableComponent } from '@src/__components/components'; +import { promiseAll } from '@seed/helpers/Utils'; +import DB from '@seed/services/database/DBService'; + +import { v4 as uuid } from 'uuid'; + +export interface GetSettingsOutput { + env: any[]; + errors: any[]; + emails: any[]; +} + +export interface EngineServerCacheInfo { + emailsContent: { + [key in NotificationEnum | EngineNotificationEnum]: any; + }; + errorsContent: { + [key: string]: { + message: string; + translations?: { + subject?: TranslatableComponent; + body?: TranslatableComponent; + }; + }; + }; +} + +export const getSettingsFromDB = async (): Promise => { + const settings = (await await (await DB.getInstance()).db.collection('settings').find().toArray()) as (any | any | any)[]; + + // filtering client side because not too much data and no need to index db + return { + env: _.filter(settings, { type: SettingsType.env }) as any[], + errors: _.filter(settings, { type: SettingsType.errors }) as any[], + emails: _.filter(settings, { type: SettingsType.emails }) as any[], + }; +}; + +export const syncEnvFromDB = (currentEnvInDB: GetSettingsOutput): void => { + // update env + currentEnvInDB.env.map((env) => { + process.env[env.key] = env.value; + }); +}; + +export const initEnvironement = async (env: any): Promise => { + const currentEnv = process.env.NODE_ENV && env[process.env.NODE_ENV] ? env[process.env.NODE_ENV] : env.default; + + for (const k in currentEnv) { + process.env[k] = currentEnv[k]; + } + + // Init also Package.json + process.env.awsRegion = packageFile.devops.awsRegion; + + const currentSettings = await getSettingsFromDB(); + syncEnvFromDB(currentSettings); + + // Force the local to be > DB + if (process.env.NODE_ENV == 'local') { + for (const k in currentEnv) { + process.env[k] = currentEnv[k]; + } + } + + console.log('[Server - Env] Loaded'); + + return currentSettings; +}; + +export class SettingsCache { + private static instance: SettingsCache; + private lastCacheDate: DateTime; + public cache: EngineServerCacheInfo; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + public static getInstance(): SettingsCache { + if (!this.instance) { + this.instance = new SettingsCache(); + } + + return this.instance; + } + + public async refreshCache(): Promise { + // check if needs to refresh the cache + if (!this.lastCacheDate || this.lastCacheDate.diff(DateTime.local(), 'minutes').minutes > 5) { + const settings = await initEnvironement(env); + + const promises: Promise[] = []; + + // Map for errors + const errorsContent = await mapErrorContent(settings, promises); + + // Map for emails + const emailsContent: { + [key in NotificationEnum | EngineNotificationEnum]: any; + } = await mapEmailSettings(settings, promises); + + this.cache = { + errorsContent, + emailsContent, + }; + this.lastCacheDate = DateTime.local(); + + if (promises.length > 0) { + console.log('Creating defaut in DB'); + await promiseAll(promises); + } + } + } +} +async function mapErrorContent(settings: GetSettingsOutput, newData: Promise[]) { + const errorsContent: { + [key: string]: { + message: string; + translations?: { + subject?: TranslatableComponent; + body?: TranslatableComponent; + }; + }; + } = {}; + for (const key in ErrorsConfig) { + if (Object.prototype.hasOwnProperty.call(ErrorsConfig, key)) { + const errIndex = _.findIndex(settings.errors, { key: key }); + errorsContent[key] = { + message: ErrorsConfig[key], + }; + if (errIndex === -1) { + console.error('[SETTINGS - ERROR] Creating default', key); + newData.push( + (await DB.getInstance()).db.collection('settings').insertOne({ + key, + type: SettingsType.errors, + value: ErrorsConfig[key], + body: new TranslatableComponent(), + _id: uuid(), + r: ['admin'], + w: ['admin'], + d: ['admin'], + createdAt: new Date(), + updatedAt: new Date(), + }), + ); + } else { + errorsContent[key].translations = { + subject: settings.errors[errIndex].subject, + body: settings.errors[errIndex].body, + }; + } + } + } + return errorsContent; +} + +async function mapEmailSettings(settings: GetSettingsOutput, newData: Promise[]) { + const emails = _.groupBy(settings.emails, 'key'); + const emailsContent: { + [key in NotificationEnum | EngineNotificationEnum]: any; + } = {} as any; + + const enumConcat = [...Object.keys(NotificationEnum), ...Object.keys(EngineNotificationEnum)]; + + enumConcat.forEach(async (element) => { + if (!emails[element]) { + console.error('[SETTINGS - EMAIL] Creating default', element); + newData.push( + (await DB.getInstance()).db.collection('settings').insertOne({ + key: element, + type: SettingsType.emails, + body: new TranslatableComponent(), + custom: false, + fromEmail: '', + fromName: '', + replyToEmail: '', + r: ['admin'], + w: ['admin'], + d: ['admin'], + _id: uuid(), + createdAt: new Date(), + updatedAt: new Date(), + }), + ); + } else emailsContent[element] = emails[element]; + }); + return emailsContent; +} diff --git a/lib/seed/graphql/baseModels/BaseContentModel.ts b/lib/seed/graphql/baseModels/BaseContentModel.ts new file mode 100644 index 0000000..f53e273 --- /dev/null +++ b/lib/seed/graphql/baseModels/BaseContentModel.ts @@ -0,0 +1,106 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import ImageComponent, { SEOField } from '@seed/interfaces/components'; +import { GetArgs, GetManyArgs } from '@seed/graphql/Request'; + +import { TranslatableComponent } from '@src/__components/components'; + +import { Permission } from '@seed/interfaces/permission'; +import BaseSEOModel, { NewBaseSEOInput, EditBaseSEOInput } from './BaseSeoModel'; +import CategoryModel from '@services/module-cms/functions/categories/category.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class BaseContentModel extends BaseSEOModel { + public constructor(collectionName?: string, perm?: Permission) { + const cName = collectionName ? collectionName : 'articles'; + const permission = perm ? perm : permissions; + super(cName, permission); + } + + @Field() + published?: boolean; + @Field({ nullable: true }) + publicationDate?: Date; + + @Field({ nullable: true }) + private?: boolean; + + @Field(() => [String], { nullable: true }) + categoryIds?: string[]; + @Field(() => [CategoryModel], { nullable: 'itemsAndList' }) + getCategories?: CategoryModel[]; + + @Field(() => [ImageComponent], { nullable: true }) + downloadableRessources?: ImageComponent[]; + + @Field(() => TranslatableComponent, { nullable: true }) + testimonials?: TranslatableComponent; + + searchOptions(): string[] { + return []; + } +} + +@ArgsType() +export class BaseContentArgs extends GetManyArgs {} + +@InputType() +export class NewBaseContentInput extends NewBaseSEOInput implements Partial { + @Field(() => TranslatableComponent, { nullable: true }) + title?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + teaser?: TranslatableComponent; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => [String], { nullable: true }) + categoryIds?: string[]; + @Field(() => [ImageComponent], { nullable: true }) + downloadableRessources?: ImageComponent[]; + + published = false; + + @Field({ nullable: true }) + private?: boolean; + + @Field(() => TranslatableComponent, { nullable: true }) + testimonials?: TranslatableComponent; +} + +@InputType() +export class EditBaseContentInput extends EditBaseSEOInput implements Partial { + @Field(() => TranslatableComponent, { nullable: true }) + title?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + teaser?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + content?: TranslatableComponent; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => [String], { nullable: true }) + categoryIds?: string[]; + @Field(() => [ImageComponent], { nullable: true }) + downloadableRessources?: ImageComponent[]; + + @Field({ nullable: true }) + private?: boolean; + + @Field(() => TranslatableComponent, { nullable: true }) + testimonials?: TranslatableComponent; +} diff --git a/lib/seed/graphql/baseModels/BaseSeoModel.ts b/lib/seed/graphql/baseModels/BaseSeoModel.ts new file mode 100644 index 0000000..3e6ea67 --- /dev/null +++ b/lib/seed/graphql/baseModels/BaseSeoModel.ts @@ -0,0 +1,103 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import ImageComponent, { SEOField } from '@seed/interfaces/components'; +import { GetArgs, GetManyArgs } from '@seed/graphql/Request'; + +import { TranslatableComponent } from '@src/__components/components'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class BaseSEOModel extends BaseGraphModel { + public constructor(collectionName?: string, perm?: Permission) { + const cName = collectionName ? collectionName : 'articles'; + const permission = perm ? perm : permissions; + super({ + // ...init, + collectionName: cName, + permissions: permission, + }); + } + + @Field(() => TranslatableComponent, { nullable: true }) + title?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + teaser?: TranslatableComponent; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => [ImageComponent], { nullable: true }) + extraImages?: ImageComponent[]; + + @Field(() => TranslatableComponent, { nullable: true }) + content?: TranslatableComponent; + + @Field(() => SEOField, { nullable: true }) + seo?: SEOField; + @Field({ nullable: true }) + urls?: TranslatableComponent; + + searchOptions(): string[] { + return []; + } + filterOptions(): string[] { + return []; + } +} + +@ArgsType() +export class BaseSEOArgs extends GetManyArgs {} + +@InputType() +export class NewBaseSEOInput implements Partial { + @Field(() => TranslatableComponent, { nullable: true }) + title?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + teaser?: TranslatableComponent; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => TranslatableComponent, { nullable: true }) + content?: TranslatableComponent; + + @Field(() => SEOField, { nullable: true }) + seo?: SEOField; + @Field({ nullable: true }) + urls?: TranslatableComponent; +} + +@InputType() +export class EditBaseSEOInput implements Partial { + @Field(() => TranslatableComponent, { nullable: true }) + title?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + teaser?: TranslatableComponent; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => TranslatableComponent, { nullable: true }) + content?: TranslatableComponent; + + @Field(() => SEOField, { nullable: true }) + seo?: SEOField; + @Field({ nullable: true }) + urls?: TranslatableComponent; +} diff --git a/lib/seed/graphql/baseModels/BaseSeoSimpleModel.ts b/lib/seed/graphql/baseModels/BaseSeoSimpleModel.ts new file mode 100644 index 0000000..c39253b --- /dev/null +++ b/lib/seed/graphql/baseModels/BaseSeoSimpleModel.ts @@ -0,0 +1,107 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import ImageComponent, { SEOSimpleField } from '@seed/interfaces/components'; +import { GetArgs, GetManyArgs } from '@seed/graphql/Request'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class BaseSEOSimpleModel extends BaseGraphModel { + public constructor(collectionName?: string, perm?: Permission) { + const cName = collectionName ? collectionName : 'articles'; + const permission = perm ? perm : permissions; + super({ + // ...init, + collectionName: cName, + permissions: permission, + }); + } + + @Field({ nullable: true }) + title?: string; + @Field({ nullable: true }) + teaser?: string; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => [ImageComponent], { nullable: true }) + extraImages?: ImageComponent[]; + + @Field({ nullable: true }) + content?: string; + + @Field(() => SEOSimpleField, { nullable: true }) + seo?: SEOSimpleField; + @Field({ nullable: true }) + urls?: string; + + searchOptions(): string[] { + return []; + } + filterOptions(): string[] { + return []; + } +} + +@ArgsType() +export class BaseSEOArgs extends GetManyArgs {} + +@InputType() +export class NewBaseSEOSimpleInput implements Partial { + @Field({ nullable: true }) + title?: string; + @Field({ nullable: true }) + teaser?: string; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => [ImageComponent], { nullable: true }) + extraImages?: ImageComponent[]; + + @Field({ nullable: true }) + content?: string; + + @Field(() => SEOSimpleField, { nullable: true }) + seo?: SEOSimpleField; + @Field({ nullable: true }) + urls?: string; +} + +@InputType() +export class EditBaseSEOSimpleInput implements Partial { + @Field({ nullable: true }) + title?: string; + @Field({ nullable: true }) + teaser?: string; + + @Field(() => ImageComponent, { nullable: true }) + cover?: ImageComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; + + @Field(() => [ImageComponent], { nullable: true }) + extraImages?: ImageComponent[]; + + @Field({ nullable: true }) + content?: string; + + @Field(() => SEOSimpleField, { nullable: true }) + seo?: SEOSimpleField; + @Field({ nullable: true }) + urls?: string; +} diff --git a/lib/seed/graphql/baseResolvers/BasePublicResolver.ts b/lib/seed/graphql/baseResolvers/BasePublicResolver.ts new file mode 100644 index 0000000..60f133b --- /dev/null +++ b/lib/seed/graphql/baseResolvers/BasePublicResolver.ts @@ -0,0 +1,143 @@ +import { Query, Arg, Int, Resolver, Authorized, Ctx, Args, Mutation, ClassType } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; +import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { totalPublic } from '../Middleware'; + +export const createBasePublicResolver = , ARGS extends ClassType>( + domain: string, + objectTypeCls: T, + argsType: ARGS, + pub = false, +): any => { + @Resolver({ isAbstract: true }) + abstract class BasePublicResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => objectTypeCls, { name: `${domain}GetOne` }) + @totalPublic(pub) + async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [objectTypeCls], { name: `${domain}GetMany` }) + @totalPublic(pub) + async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getManyGenericWithArgs(model, args, ctx); + } + + @Query(() => Number, { name: `${domain}GetCount` }) + @totalPublic(pub) + async getCount(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getCountGeneric(model, args, ctx); + } + } + + return BasePublicResolver; +}; + +export const createBaseMyResolver = , ARGS extends ClassType>( + domain: string, + objectTypeCls: T, + argsType: ARGS, +): any => { + @Resolver({ isAbstract: true }) + abstract class BaseMyResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => objectTypeCls, { name: `${domain}GetOneMine` }) + @Authorized() + async getMineOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [objectTypeCls], { name: `${domain}GetManyMine` }) + @Authorized() + async getMine(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getManyGenericWithArgs(model, args, ctx); + } + } + + return BaseMyResolver; +}; + +export const createBaseReadOnlyResolver = , ARGS extends ClassType>( + domain: string, + objectTypeCls: T, + argsType: ARGS, + auth: AccountTypeEnum[], +): any => { + @Resolver({ isAbstract: true }) + abstract class BaseReadOnlyResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => objectTypeCls, { name: `${domain}GetOne` }) + @Authorized(auth) + async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [objectTypeCls], { name: `${domain}GetMany` }) + @Authorized(auth) + async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getManyGenericWithArgs(model, args, ctx); + } + } + + return BaseReadOnlyResolver; +}; diff --git a/lib/seed/graphql/baseResolvers/BaseResolver.ts b/lib/seed/graphql/baseResolvers/BaseResolver.ts new file mode 100644 index 0000000..66dc46d --- /dev/null +++ b/lib/seed/graphql/baseResolvers/BaseResolver.ts @@ -0,0 +1,100 @@ +import { Query, Arg, Int, Resolver, Authorized, Ctx, Args, Mutation, ClassType } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; +import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { checkOrganisation as CheckOrganisation } from '../Middleware'; + +export const createBaseResolver = , ARGS extends ClassType, NEW extends ClassType, EDIT extends ClassType>( + domain: string, + objectTypeCls: T, + argsType: ARGS, + newInput: NEW, + editInput: EDIT, + auth: AccountTypeEnum[], + organisationCheck = false, +): any => { + @Resolver({ isAbstract: true }) + abstract class BaseResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => objectTypeCls, { name: `${domain}GetOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [objectTypeCls], { name: `${domain}GetMany` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getManyGenericWithArgs(model, args, ctx); + } + + @Query(() => Number, { name: `${domain}GetCount` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async getCount(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getCountGeneric(model, args, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => objectTypeCls, { name: `${domain}AddOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async addOne(@Arg('input', (type) => newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + if (model.onCreate) await model.onCreate(input); + + return addOneGeneric(model, input, ctx); + } + + @Mutation(() => objectTypeCls, { name: `${domain}EditOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async editOne(@Arg('id') id: string, @Arg('input', (type) => editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + if (model.onUpdate) await model.onUpdate(input); + + return editOneGeneric(model, id, input, ctx); + } + + @Mutation(() => objectTypeCls, { name: `${domain}DeleteOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + if (model.onDelete) await model.onDelete(); + + return deleteOneGeneric(model, id, ctx); + } + } + + return BaseResolver; +}; diff --git a/lib/seed/graphql/genericResolvers/BaseGenericResolver.ts b/lib/seed/graphql/genericResolvers/BaseGenericResolver.ts new file mode 100644 index 0000000..3db683e --- /dev/null +++ b/lib/seed/graphql/genericResolvers/BaseGenericResolver.ts @@ -0,0 +1,97 @@ +import { Query, Arg, Resolver, Ctx, Args, Mutation, ClassType } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; +import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService'; +import { EngineMiddleware, EngineMiddlewareInput } from '../MiddlewareV2'; + +export const createGenericQueryResolver = , ARGS extends ClassType>(init: { + domain: string; + modelName: T; + argsType: ARGS; + customMiddlewareInput: EngineMiddlewareInput; +}): any => { + @Resolver({ isAbstract: true }) + abstract class BaseQueryResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => init.modelName, { name: `${init.domain}GetOne` }) + @EngineMiddleware(init.customMiddlewareInput) + async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [init.modelName], { name: `${init.domain}GetMany` }) + @EngineMiddleware(init.customMiddlewareInput) + async getMany(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName(); + return getManyGenericWithArgs(model, args, ctx); + } + + @Query(() => Number, { name: `${init.domain}GetCount` }) + @EngineMiddleware(init.customMiddlewareInput) + async getCount(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName(); + return getCountGeneric(model, args, ctx); + } + } + + return BaseQueryResolver; +}; + +export const createGenericMutationResolver = , NEW extends ClassType, EDIT extends ClassType>(init: { + domain: string; + modelName: T; + newInput: NEW; + editInput: EDIT; + customMiddlewareInput: EngineMiddlewareInput; +}): any => { + @Resolver({ isAbstract: true }) + abstract class BaseMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => init.modelName, { name: `${init.domain}AddOne` }) + @EngineMiddleware(init.customMiddlewareInput) + async addOne(@Arg('input', () => init.newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName(); + return addOneGeneric(model, input, ctx); + } + + @Mutation(() => init.modelName, { name: `${init.domain}EditOne` }) + @EngineMiddleware(init.customMiddlewareInput) + async editOne(@Arg('id') id: string, @Arg('input', () => init.editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName(); + return editOneGeneric(model, id, input, ctx); + } + + @Mutation(() => init.modelName, { name: `${init.domain}DeleteOne` }) + @EngineMiddleware(init.customMiddlewareInput) + async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new init.modelName(); + return deleteOneGeneric(model, id, ctx); + } + } + + return BaseMutationResolver; +}; diff --git a/lib/seed/graphql/genericResolvers/BaseResolver.ts b/lib/seed/graphql/genericResolvers/BaseResolver.ts new file mode 100644 index 0000000..66dc46d --- /dev/null +++ b/lib/seed/graphql/genericResolvers/BaseResolver.ts @@ -0,0 +1,100 @@ +import { Query, Arg, Int, Resolver, Authorized, Ctx, Args, Mutation, ClassType } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; +import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { checkOrganisation as CheckOrganisation } from '../Middleware'; + +export const createBaseResolver = , ARGS extends ClassType, NEW extends ClassType, EDIT extends ClassType>( + domain: string, + objectTypeCls: T, + argsType: ARGS, + newInput: NEW, + editInput: EDIT, + auth: AccountTypeEnum[], + organisationCheck = false, +): any => { + @Resolver({ isAbstract: true }) + abstract class BaseResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => objectTypeCls, { name: `${domain}GetOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [objectTypeCls], { name: `${domain}GetMany` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getManyGenericWithArgs(model, args, ctx); + } + + @Query(() => Number, { name: `${domain}GetCount` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async getCount(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + return getCountGeneric(model, args, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => objectTypeCls, { name: `${domain}AddOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async addOne(@Arg('input', (type) => newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + if (model.onCreate) await model.onCreate(input); + + return addOneGeneric(model, input, ctx); + } + + @Mutation(() => objectTypeCls, { name: `${domain}EditOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async editOne(@Arg('id') id: string, @Arg('input', (type) => editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + if (model.onUpdate) await model.onUpdate(input); + + return editOneGeneric(model, id, input, ctx); + } + + @Mutation(() => objectTypeCls, { name: `${domain}DeleteOne` }) + @Authorized(auth) + @CheckOrganisation(organisationCheck) + async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new objectTypeCls(); + if (model.onDelete) await model.onDelete(); + + return deleteOneGeneric(model, id, ctx); + } + } + + return BaseResolver; +}; diff --git a/lib/seed/helpers/Context.ts b/lib/seed/helpers/Context.ts new file mode 100644 index 0000000..925111b --- /dev/null +++ b/lib/seed/helpers/Context.ts @@ -0,0 +1,12 @@ +import { ApolloContextLoadersOnly } from '@seed/interfaces/context'; +import Loaders from '@src/__indexes/__loaders'; + +export const createContext = async (): Promise => { + return { + event: {} as any, + context: {} as any, + ctx: { + loaders: new Loaders(), + }, + }; +}; diff --git a/lib/seed/helpers/Error.ts b/lib/seed/helpers/Error.ts new file mode 100644 index 0000000..44bebdc --- /dev/null +++ b/lib/seed/helpers/Error.ts @@ -0,0 +1,43 @@ +import { ErrorsConfig } from '@config/config'; + +import { SettingsCache } from '@seed/graphql/Settings'; +import { ApolloError } from 'apollo-server-lambda'; +import { UtilStringHelper } from './Utils.string'; + +type ErrorKeys = keyof typeof ErrorsConfig; + +const utilStringHelper = new UtilStringHelper(); + +export const newError = (code: ErrorKeys, data?: any): ApolloError => { + const errorConfig = SettingsCache.getInstance().cache.errorsContent; + + const message = errorConfig[code].message; + const settings = errorConfig[code].translations; + + const translations = { + subject: {}, + body: {}, + }; + + if (settings) { + if (settings.body) { + for (const key in settings.body) { + if (Object.prototype.hasOwnProperty.call(settings.body, key)) { + translations.body[key] = utilStringHelper.interpolate(settings.body[key], data); + } + } + } + if (settings.subject) { + for (const key in settings.subject) { + if (Object.prototype.hasOwnProperty.call(settings.subject, key)) { + translations.subject[key] = utilStringHelper.interpolate(settings.subject[key], data); + } + } + } + } + + return new ApolloError(utilStringHelper.interpolate(message, data), code.toString(), { + translations, + data, + }); +}; diff --git a/lib/seed/helpers/FetchHelper.ts b/lib/seed/helpers/FetchHelper.ts new file mode 100644 index 0000000..5c146d1 --- /dev/null +++ b/lib/seed/helpers/FetchHelper.ts @@ -0,0 +1,64 @@ +import fetch from 'node-fetch'; +import querystring from 'querystring'; + +// convert object to a query string +export const apiCall = async (url: string, method: 'POST' | 'PUT' | 'PATCH' | 'GET' | 'DELETE', headers?: any, body?: any): Promise => { + const q: any = { + method: method, + headers: { + 'Content-Type': 'application/json', + }, + }; + + if (headers) q.headers = headers; + if (body) { + if (typeof body === 'string') q.body = body; + else q.body = JSON.stringify(body); + } + + const response = await fetch(url, q); + + const rs = await response.json(); + if (!response.ok) throw { error: rs }; + + return rs; +}; + +export const fetchCall = async (init: { + url: string; + method: 'POST' | 'PUT' | 'PATCH' | 'GET' | 'DELETE'; + query?: any; + headers?: any; + body?: any; + raw?: boolean; +}): Promise => { + let { url } = init; + const { method, query, headers, body, raw } = init; + const q: any = { + method: method, + headers: { + 'Content-Type': 'application/json', + }, + }; + + if (query) url = url + '?' + querystring.stringify(query, undefined, undefined, { encodeURIComponent: encodeURIComponent }); + if (headers) q.headers = headers; + if (body) { + if (typeof body === 'string') q.body = body; + else q.body = JSON.stringify(body); + } + + console.log('fetchCall', url); + + const response = await fetch(url, q); + + let rs; + try { + rs = await response.json(); + } catch (error) { + rs = await response.text(); + } + if (!response.ok) throw { error: rs }; + + return rs; +}; diff --git a/lib/seed/helpers/Request.ts b/lib/seed/helpers/Request.ts new file mode 100644 index 0000000..753c463 --- /dev/null +++ b/lib/seed/helpers/Request.ts @@ -0,0 +1,29 @@ +import { GetArgs } from '@seed/graphql/Request'; + +const DEFAULT_LIMIT = 100; +const DEFAULT_OFFSET = 0; + +export interface QueryPagination { + limit: number; + skip: number; + sort?: { + [k: string]: 1 | -1; + }; +} + +export const parsePaginationOptions = (obj?: GetArgs): QueryPagination => { + const limit = obj ? (obj.limit ? obj.limit : DEFAULT_LIMIT) : DEFAULT_LIMIT; + const skip = obj ? (obj.skip ? obj.skip : DEFAULT_OFFSET) : DEFAULT_OFFSET; + + if (obj && obj.sort) { + const sort = {}; + const sortArray = ('' + obj.sort).split(','); + for (let index = 0; index < sortArray.length; index++) { + const sortTmp = ('' + obj.sort).split(' '); + if (sortTmp[1].toLowerCase() === 'asc') (sort as any)[sortTmp[0]] = 1; + if (sortTmp[1].toLowerCase() === 'desc') (sort as any)[sortTmp[0]] = -1; + } + return { limit, skip, sort }; + } + return { limit, skip }; +}; diff --git a/lib/seed/helpers/StringHelper.ts b/lib/seed/helpers/StringHelper.ts new file mode 100644 index 0000000..3e6cbe0 --- /dev/null +++ b/lib/seed/helpers/StringHelper.ts @@ -0,0 +1,28 @@ +import { TranslatableComponent } from '@src/__components/components'; +import _ from 'lodash'; + +export const getAllTranslationsToString = (str: string, input: TranslatableComponent): string => { + for (const key in input) { + if (Object.prototype.hasOwnProperty.call(input, key)) { + str = str + '#' + input[key].toLowerCase(); + } + } + return str; +}; + +export const getSearchString = (...inputs: (string | TranslatableComponent)[]): string => { + let str = ''; + + for (let index = 0; index < inputs.length; index++) { + const element = inputs[index]; + if (_.isString(element)) str = str + '#' + element.toLowerCase(); + else str = getAllTranslationsToString(str, element); + } + + return str; +}; + +//escape regex => for special characters in email +export const escapeRegex = (regex) => { + return regex.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); +} \ No newline at end of file diff --git a/lib/seed/helpers/TestsHelper.ts b/lib/seed/helpers/TestsHelper.ts new file mode 100644 index 0000000..9b9fcf0 --- /dev/null +++ b/lib/seed/helpers/TestsHelper.ts @@ -0,0 +1,44 @@ +import fetch from 'node-fetch'; + +export const graphRequest = async (query, option): Promise => { + console.log('[TESTS] Query', query); + + let params; + if (option.authHeader) + params = { + body: JSON.stringify({ query }), + method: 'POST', + headers: { + authorization: option.authHeader, + 'Content-Type': 'application/json', + }, + }; + else + params = { + body: JSON.stringify({ query }), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }; + + const response = await fetch(option.url, params); + if (!response.ok) { + return new Promise((resolve, reject) => { + response + .text() + .then((text) => { + try { + console.log('[TESTS] Error', JSON.parse(text)); + reject(JSON.parse(text)); + } catch (err) { + console.log('[TESTS] Error', text); + reject(text); + } + }) + .catch(reject); + }); + } + const json = await response.json(); + return json.data; +}; diff --git a/lib/seed/helpers/Utils.checks.ts b/lib/seed/helpers/Utils.checks.ts new file mode 100644 index 0000000..53ae52c --- /dev/null +++ b/lib/seed/helpers/Utils.checks.ts @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { newError } from './Error'; + +export const isAllThere = (listIds: string[], data: any[], modelName = 'modelName'): void => { + const ids = _.map(data, '_id'); + const nIds = _.difference(listIds, ids); + if (nIds.length > 0) throw newError(4041, { id: nIds.join(','), model: modelName }); + + return; +}; + +export const hasDuplicates = (array: any[]): boolean => { + return _.uniq(array).length !== array.length; +}; diff --git a/lib/seed/helpers/Utils.dates.intervals.ts b/lib/seed/helpers/Utils.dates.intervals.ts new file mode 100644 index 0000000..20a9726 --- /dev/null +++ b/lib/seed/helpers/Utils.dates.intervals.ts @@ -0,0 +1,88 @@ +import { DateTimeRangeComponent, AvailabilityComponent, DateRangeComponent, HourComponent } from '@seed/interfaces/components.dates'; +import _ from 'lodash'; +import { Interval, DateTime } from 'luxon'; + +export function checkIfDatesAreInAvailabilities( + dates: DateTimeRangeComponent[], + availabilities: AvailabilityComponent, + options?: { zone: string }, +): boolean { + const zone = options?.zone || process.env.TIMEZONE || 'utc'; + const intervalsExceptions = getExceptionsIntervalFromAvailabilities(availabilities, zone); + let isInException = false; + + if (intervalsExceptions.length > 0) { + isInException = _.some(dates, (d) => { + return checkIfDateRangeIsInIntervals(d, intervalsExceptions); + }); + } + + const intervalDates = getDatesIntervalFromAvailabilities(availabilities, zone); + let isInDates = true; + + if (intervalDates.length > 0) { + isInDates = _.every(dates, (d) => { + return checkIfDateRangeIsInIntervals(d, intervalDates); + }); + } + + return !isInException && isInDates; +} + +function checkIfDateRangeIsInIntervals(dateR: DateTimeRangeComponent, intervals: Interval[]): boolean { + const interval = Interval.fromDateTimes(dateR.startDate, dateR.endDate); + return _.some(intervals, (i) => i.overlaps(interval)); +} + +function getExceptionsIntervalFromAvailabilities(availabilities: AvailabilityComponent, zone: string): Interval[] { + const exceptions = availabilities.exceptions; + const intervalsExceptions: Interval[] = []; + + // Create all ezxceptions intervals + if (exceptions) { + exceptions.forEach((e) => { + createIntervalFromDatesAndHours(e.dates, zone, e.hours, intervalsExceptions); + }); + } + + return intervalsExceptions; +} + +function getDatesIntervalFromAvailabilities(availabilities: AvailabilityComponent, zone: string): Interval[] { + const dates = availabilities.dates; + const hours = availabilities.hours; + const datesExceptions: Interval[] = []; + + // Create all ezxceptions dates + if (dates) { + createIntervalFromDatesAndHours(dates, zone, hours, datesExceptions); + } + + return datesExceptions; +} + +function createIntervalFromDatesAndHours(dates: DateRangeComponent, zone: string, hours: HourComponent[] | undefined, datesExceptions: Interval[]) { + let dateStart = DateTime.fromJSDate(dates.startDate, { zone }); + const dateEnd = DateTime.fromJSDate(dates.endDate, { zone }); + + if (hours) { + while (dateStart < dateEnd) { + hours.forEach((h) => { + const hStart = parseInt(h.from.split(':')[0]); + const mStart = parseInt(h.from.split(':')[1]); + + const hEnd = parseInt(h.to.split(':')[0]); + const mEnd = parseInt(h.to.split(':')[1]); + + datesExceptions.push( + Interval.fromDateTimes(dateStart.set({ hour: hStart, minute: mStart }), dateStart.set({ hour: hEnd, minute: mEnd })), + ); + }); + dateStart = dateStart.plus({ day: 1 }); + } + + // if all day + } else { + datesExceptions.push(Interval.fromDateTimes(DateTime.fromJSDate(dates.startDate, { zone }), DateTime.fromJSDate(dates.endDate, { zone }))); + } +} diff --git a/lib/seed/helpers/Utils.dates.ts b/lib/seed/helpers/Utils.dates.ts new file mode 100644 index 0000000..c728862 --- /dev/null +++ b/lib/seed/helpers/Utils.dates.ts @@ -0,0 +1,310 @@ +import { + AvailabilityComponent, + DateRangeComponent, + DateTimeRangeComponent, + HourComponent, + SlotAvailablity, + SlotComponent, +} from '@seed/interfaces/components.dates'; +import _ from 'lodash'; +import { DateTime, Interval } from 'luxon'; + +interface hoursRange { + startHours: string; + startHoursInNumber: number; + endHours: string; + endHoursInNumber: number; +} +/* + breakTime: [ + ['11:00', '14:00'], + ['16:00', '18:00'], + ], +*/ + +export function getNumberOfWeekendDays(dateBegin: DateTime, dateEnd: DateTime): number { + let weekendDays = 0; + + let begin = dateBegin; + + while (begin <= dateEnd) { + if (begin.weekday == 6 || begin.weekday == 7) weekendDays = weekendDays + 1; + + begin = begin.plus({ day: 1 }); + } + + return weekendDays; +} + +function isInTimeInterval(slotTime: DateTime, slotInterval: number, timeRange: hoursRange[]): boolean { + const startTime = slotTime; + const endTime = slotTime.plus({ minutes: slotInterval }); + + const startTimeInNumber = startTime.hour * 60 + startTime.minute; + const endTimeInNumber = endTime.hour * 60 + endTime.minute; + + const isInInterval = timeRange.some((br) => { + return startTimeInNumber < br.endHoursInNumber && endTimeInNumber > br.startHoursInNumber; + // return (date.isSameOrAfter(startTime) && date.isSameOrBefore(endTime)) || (date.isSameOrAfter(startTime) && date.isSame(endTime)); + }); + + return isInInterval; +} + +export const getMinutesFromTimeInString = (str: string): number => { + const timeArray = str.split(':'); + return parseInt(timeArray[0]) * 60 + parseInt(timeArray[1]); +}; + +export const getDateNoTZ = (old: Date): Date => { + const userTimezoneOffset = old.getTimezoneOffset() * 60000; + return new Date(old.getTime() - userTimezoneOffset); +}; + +export const slotToDatesRange = (slot: SlotComponent): DateRangeComponent[] => { + const dates: DateRangeComponent[] = []; + const { startDate, endDate, startTime, endTime } = slot; + + const startTimes = startTime.split(':'); + const endTimes = endTime.split(':'); + + let beginDate = DateTime.fromJSDate(startDate).startOf('day'); + const stopDate = DateTime.fromJSDate(endDate).endOf('day'); + + while (beginDate < stopDate) { + dates.push({ + startDate: beginDate + .set({ hour: parseInt(startTimes[0]) }) + .set({ minute: parseInt(startTimes[1]) }) + .toJSDate(), + endDate: beginDate + .set({ hour: parseInt(endTimes[0]) }) + .set({ minute: parseInt(endTimes[1]) }) + // Remove one seconds for overlapping + .minus({ second: 1 }) + .toJSDate(), + }); + beginDate = beginDate.plus({ day: 1 }); + } + return dates; +}; + +export const getOneDaySlots = (input: { + day: DateTime; + dayUnavailability: DateTimeRangeComponent[]; + slotInfo: string[][]; + slotInterval: number; + slotDuration: number; + options?: { + zone?: string; + }; +}): string[] => { + const { day, dayUnavailability, slotInfo, slotDuration, slotInterval, options } = input; + + const zone = options?.zone || process.env.TIMEZONE || 'utc'; + + let currentDayMoment = day; + + // Check if currentDay = today + const currentDayStart = currentDayMoment.toISODate(); + const todayStart = DateTime.local().toISODate(); + + if (currentDayStart != todayStart) { + currentDayMoment = day.startOf('day'); + } + + const currentDayTimeInNumber = currentDayMoment.hour * 60 + currentDayMoment.minute; + + console.log('current day', currentDayMoment.toISO(), currentDayMoment.weekday); + + // Getting the slots for that day + const daySlot = slotInfo[day.weekday - 1]; + const currentDaySlots: { + startTime: DateTime; + endTime: DateTime; + }[] = []; + + if (daySlot && daySlot.length > 0) { + if (_.isArray(daySlot[0])) { + _.each(daySlot, (d) => { + currentDaySlots.push({ + startTime: DateTime.fromFormat(d[0], 'HH:mm', { zone }), + endTime: DateTime.fromFormat(d[1], 'HH:mm', { zone }), + }); + }); + } else { + currentDaySlots.push({ + startTime: DateTime.fromFormat(daySlot[0], 'HH:mm', { zone }), + endTime: DateTime.fromFormat(daySlot[1], 'HH:mm', { zone }), + }); + } + } + + const thisDaysUnavailabilities: { + startHours: string; + startHoursInNumber: number; + endHours: string; + endHoursInNumber: number; + }[] = getUnavailabilityOfThisDay(dayUnavailability, currentDayMoment); + + const currentDateSlots: string[] = []; + console.log('thisDaysUnavailabilities', thisDaysUnavailabilities); + + currentDaySlots.forEach((element) => { + let { startTime, endTime } = element; + + while (startTime < endTime) { + if ( + !(startTime.hour * 60 + startTime.minute < currentDayTimeInNumber) && + !isInTimeInterval(startTime, slotDuration, thisDaysUnavailabilities) && + startTime.hour * 60 + startTime.minute + slotDuration <= endTime.hour * 60 + endTime.minute + ) { + currentDateSlots.push(startTime.toFormat('HH:mm')); + } + startTime = startTime.plus({ minutes: slotInterval }); + } + }); + + return currentDateSlots; +}; + +export const getMultipleDaysSlots = (input: { + datesWanted: DateRangeComponent; + datesUnavailable: DateTimeRangeComponent[]; + slotInterval: number; + slotInfo: string[][]; + slotDuration: number; + options?: { + zone?: string; + }; +}): SlotAvailablity[] => { + const returnSlots: SlotAvailablity[] = []; + const { datesWanted, datesUnavailable, slotInterval, slotInfo, slotDuration, options } = input; + const { startDate, endDate } = datesWanted; + + const zone = options?.zone || process.env.TIMEZONE || 'utc'; + + let beginDate = DateTime.fromJSDate(startDate, { zone }); + const stopDate = DateTime.fromJSDate(endDate, { zone }); + + // Two loops + // Looping into the wanted dates and keeping an index for the slotInfo + while (beginDate < stopDate) { + // Get the slot info from the array of array + const slots = getOneDaySlots({ + day: beginDate, + dayUnavailability: datesUnavailable, + slotInfo, + slotInterval, + slotDuration, + options: { zone }, + }); + + // Adding it to the array + returnSlots.push({ + date: beginDate.toJSDate(), + slots, + }); + + beginDate = beginDate.plus({ day: 1 }).startOf('day'); + } + + return returnSlots; +}; + +export function getUnavailabilityOfThisDay(dayUnavailability: DateTimeRangeComponent[], currentDayMoment: DateTime) { + const thisDaysUnavailabilities: { + startHours: string; + startHoursInNumber: number; + endHours: string; + endHoursInNumber: number; + }[] = []; + + dayUnavailability.forEach((element) => { + const startDate = element.startDate; + const endDate = element.endDate; + + // If there are some reservations in that day or in multiple days + const isSameThanStartDate = currentDayMoment.hasSame(startDate, 'day'); + const isSameThanEndDate = currentDayMoment.hasSame(endDate, 'day'); + + const startHours = startDate.toFormat('HH:mm'); + const endHours = endDate.toFormat('HH:mm'); + const starts = startHours.split(':'); + const ends = endHours.split(':'); + + const startDateDay = startDate.startOf('day'); + const endDateDay = startDate.endOf('day'); + + const startHoursInNumber = parseInt(starts[0]) * 60 + parseInt(starts[1]); + const endHoursInNumber = parseInt(ends[0]) * 60 + parseInt(ends[1]); + + // If the reservation only last a day + if (isSameThanStartDate && isSameThanEndDate) { + thisDaysUnavailabilities.push({ + startHours, + endHours, + startHoursInNumber, + endHoursInNumber, + }); + } + + // If the reservation last multiple days + else if (currentDayMoment >= startDateDay && currentDayMoment <= endDateDay) { + // check if start date + if (isSameThanStartDate) + thisDaysUnavailabilities.push({ + startHours, + endHours: '23:59', + startHoursInNumber, + endHoursInNumber: 23 * 60 + 59, + }); + else if (isSameThanEndDate) + thisDaysUnavailabilities.push({ + startHours: '00:00', + endHours, + startHoursInNumber: 0, + endHoursInNumber, + }); + else + thisDaysUnavailabilities.push({ + startHours: '00:00', + endHours: '23:59', + startHoursInNumber: 0, + endHoursInNumber: 23 * 60 + 59, + }); + } + }); + return thisDaysUnavailabilities; +} + +export function isTimeSlotInDates(dates: DateTimeRangeComponent[], startTime: string, endTime: string) { + const startTimesInMinutes = getMinutesFromTimeInString(startTime); + const endTimesInMinutes = getMinutesFromTimeInString(endTime); + + // Check for each date if the minutes are in it + return dates.some((br) => { + const brStartTimeInMinutes = br.startDate.hour * 60 + br.startDate.minute; + const brEndTimeInMinutes = br.endDate.hour * 60 + br.endDate.minute; + return startTimesInMinutes < brEndTimeInMinutes && endTimesInMinutes > brStartTimeInMinutes; + }); +} + +export function converDateRangeToDateTime( + dates: DateRangeComponent[], + options?: { + zone?: string; + }, +): DateTimeRangeComponent[] { + const results: DateTimeRangeComponent[] = []; + const zone = options?.zone || process.env.TIMEZONE || 'utc'; + + dates.forEach((element) => { + results.push({ + startDate: DateTime.fromJSDate(element.startDate, { zone }), + endDate: DateTime.fromJSDate(element.endDate, { zone }), + }); + }); + + return results; +} diff --git a/lib/seed/helpers/Utils.string.ts b/lib/seed/helpers/Utils.string.ts new file mode 100644 index 0000000..49f5574 --- /dev/null +++ b/lib/seed/helpers/Utils.string.ts @@ -0,0 +1,38 @@ +import _ from 'lodash'; + +export class UtilStringHelper { + interpolate = function( + string: string, + data: any, + replace?: { + withEol?: boolean; + }, + ): any { + let finalString = string; + + if (!replace || !replace.withEol) finalString = finalString = finalString.replace(/\s{2,}/g, ''); + // finalString.replace(/(?:\r\n|\r|\n)/g, '
'); + // // else + // finalString = finalString.replace(/\s{2,}/g, ''); // finalString.replace(/(?:\r\n|\r|\n)/g, ''); + + try { + const compiled = _.template(finalString); + if (finalString.includes('${data.')) return compiled({ data }); + return compiled(data); + } catch (error) { + console.error(error); + + const names = Object.keys(data); + const vals = Object.values(data) as any; + for (let index = 0; index < names.length; index++) { + finalString = finalString.replace('${' + names[index] + '}', vals[index]); + } + return finalString; + // return new Function(...names, "return String.raw`${this}`")(...vals); + } + }; + + removeTabAndReturn = function(string: string): string { + return string.replace(/[\n\t]+/g, ''); + }; +} diff --git a/lib/seed/helpers/Utils.ts b/lib/seed/helpers/Utils.ts new file mode 100644 index 0000000..4478078 --- /dev/null +++ b/lib/seed/helpers/Utils.ts @@ -0,0 +1,64 @@ +import _ from 'lodash'; +import { UtilStringHelper } from './Utils.string'; + +const StringHelper = new UtilStringHelper(); + +export const interpolate = StringHelper.interpolate; + +export const sleep = async (ms: number): Promise => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + +export const promiseAll = async (promises: Promise[]): Promise => { + const results = await Promise.allSettled(promises); + + results.forEach((element) => { + if (element.status == 'rejected') console.error('[ASYNC - ERROR]', element.reason); + }); + + console.log('[PROMISE ALL - DONE]'); + + return results; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const isNullBlankOrUndefined = function(o) { + return typeof o === 'undefined' || o == null || o === ''; +}; +/** + * Deep diff between two object, using lodash + * @param {Object} object Object compared + * @param {Object} base Object to compare with + * @return {Object} Return a new object who represent the diff + */ +export const differenceBetweenObject = (object, base, ignoreBlanks = false): any => { + if (!_.isObject(object) || _.isDate(object) || Array.isArray(object)) return object; // special case dates & array + return _.transform(object as any, (result, value, key) => { + if (!_.isEqual(value, base[key])) { + if (ignoreBlanks && isNullBlankOrUndefined(value) && isNullBlankOrUndefined(base[key])) return; + result[key] = _.isObject(value) && _.isObject(base[key]) ? differenceBetweenObject(value, base[key]) : value; + } + }); +}; + +export const generateCodeNumber = () => { + return Math.floor(100000 + Math.random() * 900000); +}; + +export const clog = (data: any): void => { + console.log(JSON.stringify(data, null, 2)); +}; + +export const nullToUndefined = (value: any): any | undefined => { + if (_.isString(value) || _.isBoolean(value) || _.isDate(value) || _.isFinite(value) || _.isInteger(value)) return value; + + if (_.isArray(value)) return (value as any[]).map(nullToUndefined); + + if (_.isObject(value)) return _.mapValues(value as any, nullToUndefined); + + if (value === null) return undefined; + + return value; +}; diff --git a/lib/seed/interfaces/components.dates.ts b/lib/seed/interfaces/components.dates.ts new file mode 100644 index 0000000..5a9b421 --- /dev/null +++ b/lib/seed/interfaces/components.dates.ts @@ -0,0 +1,114 @@ +import { InputType, Int, Field, ArgsType, ObjectType } from 'type-graphql'; +import { IsDate, IsMilitaryTime } from 'class-validator'; +import { DateTime } from 'luxon'; + +@ObjectType() +@InputType('ScheduleInput') +export class ScheduleComponent { + @Field(() => [[String]], { nullable: true }) + mon?: string[][]; + @Field(() => [[String]], { nullable: true }) + tue?: string[][]; + @Field(() => [[String]], { nullable: true }) + wed?: string[][]; + @Field(() => [[String]], { nullable: true }) + thu?: string[][]; + @Field(() => [[String]], { nullable: true }) + fri?: string[][]; + @Field(() => [[String]], { nullable: true }) + sat?: string[][]; + @Field(() => [[String]], { nullable: true }) + sun?: string[][]; +} + +@ObjectType() +@InputType('HoursInput') +export class HourComponent { + @Field() + from: string; + @Field() + to: string; +} + +@ObjectType() +@InputType('DateRangeComponentInput') +@ArgsType() +export class DateRangeComponent { + @Field() + @IsDate() + startDate: Date; + @Field() + @IsDate() + endDate: Date; +} + +export class DateTimeRangeComponent { + startDate: DateTime; + endDate: DateTime; +} + +@ObjectType() +@InputType('SlotAvailablityInput') +export class SlotAvailablity { + @Field() + @IsDate() + date: Date; + + @Field(() => [String]) + slots: string[]; +} + +@ArgsType() +export class SlotAvailablityRequest extends DateRangeComponent { + @Field(() => Int) + interval: number; + + @Field(() => Int) + duration: number; +} + +@ObjectType() +@InputType('SlotComponentInput') +export class SlotComponent { + @Field() + @IsDate() + startDate: Date; + + @Field() + @IsDate() + endDate: Date; + + @Field() + @IsMilitaryTime() + startTime: string; + + @Field() + @IsMilitaryTime() + endTime: string; +} + +@ObjectType() +@InputType('BaseAvailabilityComponentInput') +export class BaseAvailabilityComponent { + @Field(() => DateRangeComponent) + dates: DateRangeComponent; + @Field(() => [HourComponent], { nullable: true }) + hours?: HourComponent[]; +} + +@ObjectType() +@InputType('DateExceptionComponentInput') +export class DateExceptionComponent extends BaseAvailabilityComponent { + @Field({ nullable: true }) + allDay?: boolean; +} + +@ObjectType() +@InputType('AvailabilityInput') +export class AvailabilityComponent extends BaseAvailabilityComponent { + @Field(() => [DateExceptionComponent], { nullable: true }) + exceptions?: DateExceptionComponent[]; + + @Field({ nullable: true }) + noWeekend?: boolean; +} diff --git a/lib/seed/interfaces/components.errors.ts b/lib/seed/interfaces/components.errors.ts new file mode 100644 index 0000000..7dc7fa0 --- /dev/null +++ b/lib/seed/interfaces/components.errors.ts @@ -0,0 +1,99 @@ +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ + +export const seedErrorConfig = { + /* HTTP like errors */ + 404: 'The document was not found', + 4040: 'The ressource ${obj.ressourceId} for the model ${obj.ressourceModel} does not exists', + 4041: 'The id "${id}" for the model "${model}" was not found', + + /* ************* */ + /* General & DB */ + /* ************* */ + 1000: 'General database error', + 1001: 'The document was not deleted', + 1002: 'This action is not allowed', + + /* ************* */ + /* Permissions - Accounts & Organisation */ + /* ************* */ + + // Accounts + 2000: 'Your permissions level are not allowed', + 2002: 'Authorization headers are required for this action', + 2003: 'The account is already registered', + 2004: 'We have no records of your account in our database', + 2005: 'There was an error creating your account', + 2006: 'The password confirmation you entered is incorrect', + 20060: 'The old confirmation you entered is incorrect', + 2007: 'There was an error updating your permission', + + // Firebase + 2010: 'Your login failed. Please verify your credentials', + 2011: 'We had issues with your token. Please retry', + 2012: 'We had issues creating your account. Please retry', + 2013: 'There was an issue with the autentication provider', + 2015: "Linking your account didn't work. Please contact administrator", + + // Organisations + 2100: 'Your permissions on the organisation level are not allowed', + 2101: "Your account doesn't operate in that organisation", + 2102: 'Organisation headers are required for this action', + + // Api keys + 2200: 'Your api keys are not in our system', + 2202: 'Api keys are required for this action', + + // Codes + 2300: 'The code you entered does not exists', + 2301: 'The code you entered has expired', + 2302: 'Too many attempts, please retry in 15 mins', + + /* ************* */ + /* Security */ + /* ************* */ + 2400: 'You need to complete security protocols before doing that action', + + /* ************* */ + /* DataValidation */ + /* ************* */ + + 3000: 'There are some inputs missing', + 3001: 'This is a validation error', + + 3005: 'There was an error with the errors range time', + + // -- Forms + 3100: 'Items are required for that form type', + 3101: 'The form has duplicate name entries', + 3102: "The section '${sName}' for this form doesn't exists", + 3103: "The question '${qName}' for the section '${sName}' for this form doesn't exists", + 3104: "The answers of the section '${sectionName}' doesn't exists", + 31040: "The answer ${fieldName} of the section '${sectionName}' doesn't exists", + 3105: 'The answer "${name}" for this field "${formName}" is not acceptable', + + 3106: "The tab ${tabName} for this form doesn't exists", + 3107: 'Please select a section to answer', + 3108: 'Some input are missing, ${name} for section ${sectionName}', + 3109: 'Data integrity breach, ${name} should be ${type}', + 3110: 'Only one answer (key) please', + + // -- Files + 3200: 'System cannot read the extension of the file', + 3201: 'The file extensions are not allowed - allowed : ${allowedMime}', +}; diff --git a/lib/seed/interfaces/components.geo.ts b/lib/seed/interfaces/components.geo.ts new file mode 100644 index 0000000..c466b31 --- /dev/null +++ b/lib/seed/interfaces/components.geo.ts @@ -0,0 +1,192 @@ +import { InputType, Int, Field, ArgsType, ObjectType, Args } from 'type-graphql'; + +import { Max, IsNotEmpty } from 'class-validator'; + +import { registerEnumType } from 'type-graphql'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { TranslatableComponent } from '@src/__components/components'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { IsRefExist } from '@seed/engine/decorators/db.guard'; +import { ScheduleComponent } from './components.dates'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ + +/* + * ADDRESS + */ +@InputType('LocComponentInput') +@ObjectType() +export class LocComponent { + @Field() + type: string; + @Field(() => [Number]) + coordinates: number[]; +} + +@InputType('AddressInput') +@ObjectType() +export class AddressComponent { + @Field({ nullable: true }) + number?: string; + @Field({ nullable: true }) + street?: string; + @Field({ nullable: true }) + streetBis?: string; + @Field({ nullable: true }) + floor?: string; + @Field({ nullable: true }) + box?: string; + @Field({ nullable: true }) + zip?: string; + @Field({ nullable: true }) + state?: string; + @Field() + city: string; + @Field() + country: string; + + returnFullAddress() { + return `${this.number} ${this.street}, ${this.zip} ${this.city} ${this.country}` + } +} + +@InputType('AddressStrictInput') +@ObjectType() +export class AddressStrictComponent { + @Field() + number: string; + @Field() + street: string; + @Field() + zip: string; + @Field() + city: string; + @Field() + country: string; + + @Field({ nullable: true }) + streetBis?: string; + @Field({ nullable: true }) + floor?: string; + @Field({ nullable: true }) + box?: string; + @Field({ nullable: true }) + state?: string; + + + returnFullAddress() { + return `${this.number} ${this.street}, ${this.zip} ${this.city} ${this.country}` + } +} + +@InputType('PlaceInput') +@ObjectType() +export class PlaceComponent { + @Field() + placeId: string; + @Field({ nullable: true }) + address: AddressComponent; + @Field(() => LocComponent) + loc: LocComponent; + @Field() + formattedAddress: string; +} + +@InputType('PlaceComponentOptionnalInput') +@ObjectType() +export class PlaceComponentOptionnal { + @Field({ nullable: true }) + placeId?: string; + @Field(() => AddressComponent, { nullable: true }) + address: AddressComponent; + @Field(() => LocComponent, { nullable: true }) + loc?: LocComponent; + @Field({ nullable: true }) + formattedAddress?: string; +} + +@InputType('PlaceComponentOptionnalWithAddressInput') +@ObjectType() +export class PlaceComponentOptionnalWithAddress { + @Field({ nullable: true }) + placeId?: string; + @Field(() => AddressStrictComponent) + address: AddressStrictComponent; + @Field(() => LocComponent, { nullable: true }) + loc?: LocComponent; + @Field({ nullable: true }) + formattedAddress?: string; +} + +@InputType('GeolocSearchInput') +@ObjectType() +@ArgsType() +export class GeolocSearchComponent { + @Field() + longitude: number; + @Field() + latitude: number; + + @Max(10000) + @Field(() => Int, { nullable: true }) + radius = 5000; +} + +@InputType('GeolocAddressSearchInput') +@ObjectType() +export class GeolocAddressSearchComponent { + @Field() + formattedAddress: string; + + @Max(10000) + @Field(() => Int, { nullable: true }) + radius = 5000; +} + +@InputType('GeolocPlaceSearchInput') +@ObjectType() +export class GeolocPlaceSearchComponent { + @Field() + placeId: string; + + @Max(10000) + @Field(() => Int, { nullable: true }) + radius = 5000; +} + +@ObjectType() +export class GeolocDistComponent { + @Field({ nullable: true }) + calculated?: number; + @Field(() => LocComponent, { nullable: true }) + location?: LocComponent; +} + +@InputType('PlaceContactInformationInput') +@ObjectType() +export class PlaceContactInformation { + @Field() + phoneNumber: string; + + @Field() + email: string; + + @Field(() => ScheduleComponent) + openingHours: ScheduleComponent; +} diff --git a/lib/seed/interfaces/components.notifications.ts b/lib/seed/interfaces/components.notifications.ts new file mode 100644 index 0000000..5fa0091 --- /dev/null +++ b/lib/seed/interfaces/components.notifications.ts @@ -0,0 +1,24 @@ +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +export enum EngineNotificationEnum { + 'emailCodeSignIn' = 'emailCodeSignIn', + 'magicLink' = 'magicLink', + 'resetPassword' = 'resetPassword', + 'adminAccountAddOne' = 'adminAccountAddOne', +} + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ diff --git a/lib/seed/interfaces/components.texts.ts b/lib/seed/interfaces/components.texts.ts new file mode 100644 index 0000000..7dd5c1e --- /dev/null +++ b/lib/seed/interfaces/components.texts.ts @@ -0,0 +1,46 @@ +import { InputType, Int, Field, ArgsType, ObjectType, Args } from 'type-graphql'; + +import { Max, IsDate, IsMilitaryTime, IsNotEmpty } from 'class-validator'; + +import { registerEnumType } from 'type-graphql'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { TranslatableComponent } from '@src/__components/components'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { IsRefExist } from '@seed/engine/decorators/db.guard'; +import { DateTime } from 'luxon'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ + +@ObjectType() +@InputType('GenericValueLabelComponentInput') +export class GenericValueLabelComponent { + @Field() + value: string; + @Field() + label: TranslatableComponent; +} + +@ObjectType() +@InputType('GenericTitleSubtitleComponentInput') +export class GenericTitleSubtitleComponent { + @Field(() => TranslatableComponent, { nullable: true }) + title: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + subtitle: TranslatableComponent; +} diff --git a/lib/seed/interfaces/components.ts b/lib/seed/interfaces/components.ts new file mode 100644 index 0000000..ee737bf --- /dev/null +++ b/lib/seed/interfaces/components.ts @@ -0,0 +1,318 @@ +import { InputType, Int, Field, ArgsType, ObjectType, Args } from 'type-graphql'; + +import { Max, IsNotEmpty } from 'class-validator'; + +import { registerEnumType } from 'type-graphql'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { TranslatableComponent } from '@src/__components/components'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { IsRefExist } from '@seed/engine/decorators/db.guard'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +export enum SettingsType { + emails = 'emails', + ac = 'ac', + env = 'env', + instagram = 'instagram', + errors = 'errors', + others = 'others', +} +registerEnumType(SettingsType, { + name: 'SettingsType', +}); + +export enum AccountGenderEnum { + m = 'm', + f = 'f', + other = 'other', +} +registerEnumType(AccountGenderEnum, { + name: 'AccountGenderEnum', +}); + +export enum deviceOs { + ios = 'ios', + android = 'android', +} +registerEnumType(deviceOs, { + name: 'deviceOs', +}); + +/* + * Permissions + */ + +export enum PermissionType { + r = 'r', + w = 'w', + d = 'd', +} +registerEnumType(PermissionType, { + name: 'PermissionType', +}); + +/* + * Date stuff + */ + +export enum RangeType { + strict = 'strict', // IS << A - B >> IE + intersect = 'intersect', // IS << A - IE << B || + intersectLarge = 'intersectLarge', + included = 'included', // IS << A - B >> IE +} +registerEnumType(RangeType, { + name: 'RangeType', +}); + +export enum DateTabType { + today = 'today', + tomorrow = 'tomorrow', + upcomming = 'upcomming', + past = 'past', +} +registerEnumType(DateTabType, { + name: 'DateTabType', +}); + +export enum SizeTypeEnum { + cm = 'cm', + inch = 'inch', +} +registerEnumType(SizeTypeEnum, { + name: 'SizeTypeEnum', +}); + +export enum WeightTypeEnum { + lbs = 'lbs', + g = 'g', +} +registerEnumType(WeightTypeEnum, { + name: 'WeightTypeEnum', +}); + +export enum QualityEnum { + verygood = 'verygood', + good = 'good', + neutral = 'neutral', + poor = 'poor', + verypoor = 'verypoor', +} +registerEnumType(QualityEnum, { + name: 'QualityEnum', +}); + +export enum StatusEnum { + draft = 'draft', + processing = 'processing', + validated = 'validated', + cancelled = 'cancelled', + expired = 'expired', + +} +registerEnumType(StatusEnum, { + name: 'StatusEnum', +}); + +// export enum IntervalEnum { +// '15' = '15', +// '30' = '30', +// '45' = '45', +// '60' = '60', +// '90' = '90', +// '120' = '120', +// } +// registerEnumType(IntervalEnum, { +// name: 'IntervalEnum', +// }); + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ + +@ObjectType() +export class EnginePathComponent { + @Field(() => ModelCollectionEnum) + @IsNotEmpty() + ressourceModel: ModelCollectionEnum | string; + + @Field() + @IsNotEmpty() + // @IsRefExist() + ressourceId: string; +} + +/* + * AUTH + * + */ + +@ObjectType() +export class FirebaseTokenResult { + @Field() + localId: string; + @Field({ nullable: true }) + email?: string; + @Field({ nullable: true }) + displayName?: string; + @Field() + idToken: string; + @Field({ nullable: true }) + registered?: boolean; + @Field() + refreshToken: string; + @Field() + expiresIn: string; +} + +/* + * MISC + */ +@InputType('PositionInInput') +export class PositionInInterface { + @Field() + _id: string; + + @Field(() => Int) + order: number; +} +[]; + +// @ObjectType() +// @InputType('TranslatableInput') +// export class TranslatableComponent { +// @Field() +// en: string; +// } + +/* + * RESULTS + */ + +@ObjectType('IdResult') +export class IdResult { + @Field() + _id: string; +} + +@ObjectType() +export class SimpleResult { + @Field() + message: string; +} + +/* + * DEVICES + */ + +@ObjectType() +@InputType('DeviceInput') +export class DeviceComponent { + @Field() + uid: string; + @Field(() => deviceOs) + os: deviceOs; + + @Field() + pushToken: string; + @Field() + deviceName: string; +} + +/* + * IMAGES + */ + +@ObjectType() +@InputType('ImageInput') +export default class ImageComponent { + @Field({ nullable: true }) + title?: string; + @Field({ nullable: true }) + fileType?: string; + + @Field() + large: string; + + @Field({ nullable: true }) + medium?: string; + + @Field({ nullable: true }) + small?: string; +} + +@ObjectType() +@InputType('NewPermissionInputC') +export class NewPermissionInput { + @Field(() => PermissionType) + permissionType: PermissionType; + + @Field(() => AccountTypeEnum) + permission: AccountTypeEnum; +} + +@ObjectType() +@InputType('SEOInput') +export class SEOField { + @Field(() => TranslatableComponent) + title: TranslatableComponent; + @Field(() => TranslatableComponent) + description: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + keywords?: TranslatableComponent; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; +} + +@ObjectType() +@InputType('SEOSimpleInput') +export class SEOSimpleField { + @Field() + title: string; + @Field() + description: string; + @Field({ nullable: true }) + keywords?: string; + @Field(() => ImageComponent, { nullable: true }) + thumbnail?: ImageComponent; +} + +@ObjectType() +@InputType('SizeComponentInput') +export class SizeComponent { + @Field() + unit: SizeTypeEnum; + @Field() + value: number; +} + +@ObjectType() +@InputType('WeightComponentInput') +export class WeightComponent { + @Field() + unit: WeightTypeEnum; + @Field() + value: number; +} +@ObjectType() +@InputType('CustomTagsInput') +export class CustomTagsInterface { + @Field() + key: string; + + @Field() + value: string; +} diff --git a/lib/seed/interfaces/context.ts b/lib/seed/interfaces/context.ts new file mode 100644 index 0000000..4275002 --- /dev/null +++ b/lib/seed/interfaces/context.ts @@ -0,0 +1,22 @@ +import { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import AccountModel from '@src/accounts/account.model'; + +export interface ApolloContext { + event: APIGatewayProxyEvent; + context: Context; + ctx: { + organisationId: string | null; + user: AccountModel; + loaders: any; + noOrganisationCheck?: boolean; + noPermissionCheck?: boolean; + }; +} + +export interface ApolloContextLoadersOnly { + event: APIGatewayProxyEvent; + context: Context; + ctx: { + loaders: any; + }; +} diff --git a/lib/seed/interfaces/permission.ts b/lib/seed/interfaces/permission.ts new file mode 100644 index 0000000..eec122d --- /dev/null +++ b/lib/seed/interfaces/permission.ts @@ -0,0 +1,24 @@ +import { AccountTypeEnum } from '@src/accounts/account.components'; + +export interface Permission { + c: (AccountTypeEnum | string)[]; + r: (AccountTypeEnum | string)[]; + w: (AccountTypeEnum | string)[]; + d: (AccountTypeEnum | string)[]; +} +export interface PermissionDB { + r: AccountTypeEnum[]; + w: AccountTypeEnum[]; + d: AccountTypeEnum[]; +} + +export interface PermissionIN { + operation: 'add' | 'remove'; + field: 'r' | 'w' | 'd'; + newPerm: string; +} + +export interface PermissionInfo { + accountId: string; + accountGroup: string[]; +} diff --git a/lib/seed/interfaces/response.ts b/lib/seed/interfaces/response.ts new file mode 100644 index 0000000..719a7f1 --- /dev/null +++ b/lib/seed/interfaces/response.ts @@ -0,0 +1,28 @@ +import { Field, ObjectType } from 'type-graphql'; + +export interface ErrorResponse { + statusCode: 400 | 403 | 404 | 500; + body: string; + [k: string]: any; +} + +@ObjectType() +export class SuccessResponse { + @Field() + success: boolean; +} + +export interface FunctionResponse { + success: boolean; + message: string; + data: T; +} + +export const fError = (message: string, data?: any): FunctionResponse => { + console.error(message, data); + return { success: false, message: message, data }; +}; + +export const fSuccess = (data?: any, message?: string): FunctionResponse => { + return { success: true, message: message || 'success', data }; +}; diff --git a/lib/seed/services/auth/FirebaseAuthService.ts b/lib/seed/services/auth/FirebaseAuthService.ts new file mode 100644 index 0000000..64c1b9c --- /dev/null +++ b/lib/seed/services/auth/FirebaseAuthService.ts @@ -0,0 +1,147 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { apiCall } from '@seed/helpers/FetchHelper'; +import { FirebaseTokenResult } from '@seed/interfaces/components'; +import { ApolloError } from 'apollo-server-lambda'; +import Firebase from './FirebaseService'; +import { LoginInput, LinkEmailInput, SMSVerif, CodeConfirmInput } from '@services/module-accounts/account.components'; + +import { v4 as uuid } from 'uuid'; +import { newError } from '@seed/helpers/Error'; + +const firebaseAuthUrl = 'https://identitytoolkit.googleapis.com/v1'; + +import { google, identitytoolkit_v3 } from 'googleapis'; +import { SuccessResponse } from '@seed/interfaces/response'; + +export default class FirebaseAuth { + private static instance: FirebaseAuth; + private apiKey: string; + + private identityToolkit: identitytoolkit_v3.Identitytoolkit; + + private constructor() { + this.apiKey = process.env.FIREBASE_apiKey || ''; + this.identityToolkit = google.identitytoolkit({ + auth: this.apiKey || '', + version: 'v3', + }); + } + + public static getInstance(): FirebaseAuth { + if (!FirebaseAuth.instance) { + FirebaseAuth.instance = new FirebaseAuth(); + } + + return FirebaseAuth.instance; + } + /* + * Login functions + */ + + public async loginWithEmailPassword({ email, password }: LoginInput): Promise { + try { + const requestUrl = `${firebaseAuthUrl}/accounts:signInWithPassword?key=${this.apiKey}`; + const res = await apiCall(requestUrl, 'POST', null, { + email, + password, + returnSecureToken: true, + }); + return res; + } catch (err) { + throw newError(2010, err); + } + } + + public async loginWithRefreshToken(refreshToken: string): Promise { + try { + const requestUrl = `${firebaseAuthUrl}/token?key=${this.apiKey}`; + const res = await apiCall(requestUrl, 'POST', null, { + grant_type: 'refresh_token', + refresh_token: refreshToken, + }); + + return { + localId: res.user_id, + idToken: res.id_token, + refreshToken: res.refresh_token, + expiresIn: res.expires_in, + }; + } catch (err) { + // throw newError('auth.error.' + err.error.error.message, err.error.error.code, err); + throw newError(2010, err); + } + } + + public async loginAnonymous(): Promise { + try { + const requestUrl = `${firebaseAuthUrl}/accounts:signUp?key=${this.apiKey}`; + const res = await apiCall(requestUrl, 'POST', null, { + returnSecureToken: true, + }); + + return res; + } catch (err) { + // throw newError('auth.error.' + err.error.error.message, err.error.error.code, err); + throw newError(2010, err); + } + } + + public async linkWithEmailPassword({ idToken, email, password }: LinkEmailInput): Promise { + try { + const requestUrl = `${firebaseAuthUrl}/accounts:update?key=${this.apiKey}`; + const res = await apiCall(requestUrl, 'POST', null, { + idToken, + email, + password, + returnSecureToken: true, + }); + return res; + } catch (err) { + throw newError(2015, err); + } + } + + /* + * MFA SMS functions + */ + + public async sendSMSVerification({ phoneNumber, recaptchaToken }: SMSVerif): Promise { + const response = new SuccessResponse(); + + const gRes = await this.identityToolkit.relyingparty.sendVerificationCode({ + requestBody: { + phoneNumber, + recaptchaToken, + }, + }); + + // save sessionInfo into db. You will need this to verify the SMS code + const sessionInfo = gRes.data.sessionInfo; + + console.log(sessionInfo); + + response.success = true; + + return response; + } + + public async checkSMSVerification({ email, code }: CodeConfirmInput): Promise { + const response = new SuccessResponse(); + + const sessionInfo = ''; + + const log = await this.identityToolkit.relyingparty.verifyPhoneNumber({ + requestBody: { + code: code.toString(), + phoneNumber: '', + sessionInfo, + }, + }); + + response.success = true; + + console.log(log); + + return response; + } +} diff --git a/lib/seed/services/auth/FirebaseService.ts b/lib/seed/services/auth/FirebaseService.ts new file mode 100644 index 0000000..e992977 --- /dev/null +++ b/lib/seed/services/auth/FirebaseService.ts @@ -0,0 +1,211 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import FirebaseAdmin from 'firebase-admin'; +import axios from 'axios'; +import { newError } from '@seed/helpers/Error'; + +export interface FirebaseUser { + aud: string; + auth_time: number; + email: string | undefined; + email_verified: boolean | undefined; + exp: number; + firebase: { + identities: any; + sign_in_provider: 'custom' | 'password' | 'phone' | 'anonymous' | 'google.com' | 'facebook.com' | 'github.com' | 'twitter.com'; + }; + iat: number; + name: string; + picture: string | undefined; + uid: string; +} + +export default class Firebase { + private static instance: Firebase; + + private constructor() { + try { + const base = { + type: 'service_account', + project_id: process.env.FIREBASE_project_id || '', + private_key_id: process.env.FIREBASE_private_key_id || '', + private_key: process.env.FIREBASE_private_key ? process.env.FIREBASE_private_key.replace(/\\n/g, '\n') : '', + client_email: process.env.FIREBASE_client_email || '', + client_id: process.env.FIREBASE_client_id || '', + auth_uri: process.env.FIREBASE_auth_uri || '', + token_uri: process.env.FIREBASE_token_uri || '', + auth_provider_x509_cert_url: process.env.FIREBASE_auth_provider_x509_cert_url || '', + client_x509_cert_url: process.env.FIREBASE_client_x509_cert_url || '', + apiKey: process.env.FIREBASE_apiKey || '', + }; + + FirebaseAdmin.initializeApp({ + credential: FirebaseAdmin.credential.cert({ + projectId: base.project_id, + clientEmail: base.client_email, + privateKey: base.private_key, + }), + }); + } catch (error) { + if (!/already exists/.test(error.message)) { + console.error('Firebase initialization error raised', error.stack); + } + } + } + + public static getInstance(): Firebase { + if (!Firebase.instance) { + Firebase.instance = new Firebase(); + } + + return Firebase.instance; + } + /* + * IDENTIFY TOKEN + */ + + public async tokenIdentify(token: string): Promise { + try { + const res = await FirebaseAdmin.auth().verifyIdToken(token); + return res; + } catch (err) { + throw await newError(2011, err); + } + } + + /* + * ADMIN FUNCTIONS + */ + + public async createUser(obj: { email: string; password: string }): Promise { + try { + return await FirebaseAdmin.auth().createUser({ + ...obj, + emailVerified: true, + }); + } catch (err) { + throw newError(2012, err); + } + } + + public async getUserByEmail(email: string): Promise { + try { + return await FirebaseAdmin.auth().getUserByEmail(email); + } catch (err) { + throw newError(2013, err); + } + } + + public async updatePassword(obj: { uid: string; password: string }): Promise { + try { + return await FirebaseAdmin.auth().updateUser(obj.uid, { + password: obj.password, + emailVerified: true, + }); + } catch (err) { + throw newError(2013, err); + } + } + + public async updateEmail(obj: { uid: string; email: string }): Promise { + try { + return await FirebaseAdmin.auth().updateUser(obj.uid, { + email: obj.email, + emailVerified: true, + }); + } catch (err) { + throw newError(2013, err); + } + } + + public async deleteUser(obj: { uid: string }): Promise { + try { + return await FirebaseAdmin.auth().deleteUser(obj.uid); + } catch (err) { + throw newError(2013, err); + } + } + + public async getResetPasswordLink(email: string): Promise { + try { + return await FirebaseAdmin.auth().generatePasswordResetLink(email); + } catch (err) { + throw newError(2013, err); + } + } + + public async getEmailConfirmationLink(email: string): Promise { + try { + return await FirebaseAdmin.auth().generateEmailVerificationLink(email); + } catch (err) { + throw newError(2013, err); + } + } + + public async createTokenId(email: string): Promise { + try { + const firebaseUser = await FirebaseAdmin.auth().getUserByEmail(email); + + const customToken = await FirebaseAdmin.auth().createCustomToken(firebaseUser.uid); + + const r = await axios.post( + `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=${process.env.FIREBASE_apiKey}`, + { + token: customToken, + returnSecureToken: true, + }, + ); + // console.log(r.data); + return r.data; + } catch (err) { + throw err; + } + } + + public async getMagicLink(email): Promise { + try { + const actionCodeSettings: any = { + // URL you want to redirect back to. The domain (www.example.com) for + // this URL must be whitelisted in the Firebase Console. + // This must be true for email link sign-in. + url: process.env.MAGIC_LINK_URL ? process.env.MAGIC_LINK_URL : 'https://makeit-studio.com/magicLink', + handleCodeInApp: true, + // FDL custom domain. + // dynamicLinkDomain: 'coolapp.page.link' + }; + + if (process.env.IOS_PACKAGE_NAME && process.env.IOS_PACKAGE_NAME != '') + actionCodeSettings.iOS = { + bundleId: process.env.IOS_PACKAGE_NAME, + }; + if (process.env.ANDROID_PACKAGE_NAME && process.env.ANDROID_PACKAGE_NAME != '') + actionCodeSettings.android = { + packageName: process.env.ANDROID_PACKAGE_NAME, + installApp: true, + }; + + return await FirebaseAdmin.auth().generateSignInWithEmailLink(email, actionCodeSettings); + } catch (err) { + throw err; + } + } + + /* + * PUSH NOTIFICATION FUNCTIONS + */ + + public async sendPushNotification(registrationToken: string | string[], title: string, body: string, data = {}): Promise { + try { + const firebaseInfo = await FirebaseAdmin.messaging().sendToDevice(registrationToken, { + data, + notification: { + title: title, + body: body, + }, + }); + console.log('firebase success', firebaseInfo); + } catch (error) { + console.log('firebase error', error); + } + return true; + } +} diff --git a/lib/seed/services/change-stream/change-stream.components.ts b/lib/seed/services/change-stream/change-stream.components.ts new file mode 100644 index 0000000..e15b33f --- /dev/null +++ b/lib/seed/services/change-stream/change-stream.components.ts @@ -0,0 +1,66 @@ +import { ObjectType, Field } from 'type-graphql'; + +import { registerEnumType } from 'type-graphql'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +export enum StreamOperationType { + insert = 'insert', + delete = 'delete', + replace = 'replace', + update = 'update', + hook = 'hook', +} +registerEnumType(StreamOperationType, { + name: 'StreamOperationType', +}); + +export enum PostHookStatus { + new = 'new', + inProcess = 'inProcess', + completed = 'completed', + error = 'error', + noAction = 'noAction', +} +registerEnumType(PostHookStatus, { + name: 'PostHookStatus', +}); + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ + +/* + * AUTH + * + */ + +// @ObjectType() +// export class PostHookStatus { +// @Field() +// localId: string; +// @Field({ nullable: true }) +// email?: string; +// @Field({ nullable: true }) +// displayName?: string; +// @Field() +// idToken: string; +// @Field({ nullable: true }) +// registered?: boolean; +// @Field() +// refreshToken: string; +// @Field() +// expiresIn: string; +// } diff --git a/lib/seed/services/change-stream/change-stream.model.ts b/lib/seed/services/change-stream/change-stream.model.ts new file mode 100644 index 0000000..3906923 --- /dev/null +++ b/lib/seed/services/change-stream/change-stream.model.ts @@ -0,0 +1,68 @@ +import { ObjectType, Field, ArgsType } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetArgs, GetManyArgs } from '@seed/graphql/Request'; + +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { StreamOperationType, PostHookStatus } from './change-stream.components'; +import GraphQLJSON from 'graphql-type-json'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class ChangeStreamModel extends BaseGraphModel { + public constructor() { + super({ + //!!\\ Hard coded, do not change ! + collectionName: 'stream.changes', + permissions: permissions, + }); + } + + @Field(() => StreamOperationType) + operation: StreamOperationType; + + @Field() + collection: string; + + @Field() + documentKey: string; + + @Field(() => PostHookStatus) + hookStatus: PostHookStatus; + + @Field({ nullable: true }) + logMessage?: string; + + @Field(() => GraphQLJSON, { nullable: true }) + insertedValues?: Partial; + @Field(() => GraphQLJSON, { nullable: true }) + updatedValues?: Partial; + @Field(() => GraphQLJSON, { nullable: true }) + beforeUpdateValues?: Partial; + + logInfo?: any; + + searchOptions(): string[] { + return ['documentKey', 'hookStatus']; + } + filterOptions(): string[] { + return ['documentKey', 'hookStatus']; + } +} + +@ArgsType() +export class ChangeStreamArgs extends GetManyArgs { + @Field({ nullable: true }) + documentKey?: string; + + @Field({ nullable: true }) + hookStatus?: string; +} diff --git a/lib/seed/services/change-stream/change-stream.resolver.ts b/lib/seed/services/change-stream/change-stream.resolver.ts new file mode 100644 index 0000000..7b35b6a --- /dev/null +++ b/lib/seed/services/change-stream/change-stream.resolver.ts @@ -0,0 +1,27 @@ +import { Resolver } from 'type-graphql'; +import { ChangeStreamArgs } from './change-stream.model'; +import ChangeStreamModel from './change-stream.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { createBaseReadOnlyResolver } from '@seed/graphql/baseResolvers/BasePublicResolver'; + +const ChangeStreamBaseResolver = createBaseReadOnlyResolver('changeStreams', ChangeStreamModel, ChangeStreamArgs, [AccountTypeEnum.admin]); + +@Resolver(ChangeStreamModel) +export default class ChangeStreamAdminResolver extends ChangeStreamBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/lib/seed/services/change-stream/change-stream.service.ts b/lib/seed/services/change-stream/change-stream.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/seed/services/database/DBRequestService.ts b/lib/seed/services/database/DBRequestService.ts new file mode 100644 index 0000000..bf2c187 --- /dev/null +++ b/lib/seed/services/database/DBRequestService.ts @@ -0,0 +1,561 @@ +import { GetArgs } from '@lib/seed/graphql/Request'; +import { parsePaginationOptions } from '@lib/seed/helpers/Request'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { EngineModel } from '@seed/engine/EngineModel'; +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { newError } from '@seed/helpers/Error'; +import { escapeRegex } from '@seed/helpers/StringHelper'; +import { DateTabType, RangeType } from '@seed/interfaces/components'; +import { GeolocSearchComponent } from '@seed/interfaces/components.geo'; +import { getGeoLocFromAddress } from '@seed/services/geolocation/services/GooglePlaceService'; +import { ApolloError } from 'apollo-server-lambda'; +import moment from 'moment'; +import { AggregationCursor, Collection } from 'mongodb'; + +type searchType = 'dateTab' | 'dateRange' | 'hourRange' | 'beetween' | 'bool' | 'regex' | 'nameRegex'; +interface SearchEngineValue { + operation: + | 'dateTab' + | 'dateRange' + | 'hourRange' + | 'between' + | '$eq' + | '$gt' + | '$gte' + | '$in' + | '$lt' + | '$lte' + | '$ne' + | '$nin' + | '$not' + | '$exists' + | 'bool' + | 'regex' + | 'nameRegex' + | 'freeInput' + | 'searchText'; + dateTabConfig?: { + dbFromField: string; + dbToField: string; + }; + dateRangeConfig?: { + inputFromField: keyof T; + inputToField: keyof T; + dbFromField: string; + dbToField: string; + dateRangeType?: RangeType; + }; + hourRangeConfig?: { + inputFromField: keyof T; + inputToField: keyof T; + dbMainField: string; + dbSubfieldFrom: string; + dbSubfieldTo: string; + dateRangeType?: RangeType; + }; + beetweenConfig?: { + inputFromField: keyof T; + inputToField: keyof T; + }; + freeInputConfig?: { + searchOptions: string[]; + }; + dbFName?: string; + forcedValue?: any; +} +export type CustomSearchEngine = { + [P in keyof T]: SearchEngineValue; +} & + { + [P in searchType]?: SearchEngineValue; + } & { + [key: string]: SearchEngineValue; + }; + +export const createSearchRequest = (model: T, args: { search?: string }): any[] | undefined => { + if (!args.search) return; + + const search = args.search; + const searchValue = escapeRegex(search); + + if (model.searchOptions) { + const searchOptions = model.searchOptions(); + if (searchOptions.length > 0) { + const $or: any[] = []; + for (let index = 0; index < searchOptions.length; index++) { + const element = searchOptions[index]; + $or.push({ [`${element}`]: { $regex: `.*${searchValue}.*`, $options: 'i' } }); + } + return $or; + } + } + + return; +}; +export const addSearchRequest = (model: T, args: { search?: string }, filters: { $and: any[] }): any | undefined => { + const $or = createSearchRequest(model, args); + if ($or) filters.$and.push({ $or }); +}; + +export const createDateRangeRequest = (fromField: string, toField: string, args: any, type: RangeType = RangeType.intersect): any | undefined => { + try { + if ((args[fromField] && !args[toField]) || (args[toField] && !args[fromField])) throw newError(3005, '403'); + if (args[fromField] && args[toField]) { + if (type == RangeType.strict) + return { [`${fromField}`]: { $gte: moment(args[fromField]).toDate() }, [`${toField}`]: { $lte: moment(args[toField]).toDate() } }; + else if (type == RangeType.intersect) + return { + $or: [ + { [`${fromField}`]: { $gte: moment(args[fromField]).toDate(), $lte: moment(args[toField]).toDate() } }, + { [`${toField}`]: { $gte: moment(args[fromField]).toDate(), $lte: moment(args[toField]).toDate() } }, + ], + }; + else + return { + $or: [ + { [`${fromField}`]: { $gte: moment(args[fromField]).toDate() } }, + { [`${toField}`]: { $lte: moment(args[toField]).toDate() } }, + ], + }; + } + + return; + } catch (error) { + throw error; + } +}; +export const addDateRangeRequest = ( + fromField: string, + toField: string, + args: any, + filters: { $and: any[] }, + type: RangeType = RangeType.intersect, +): any | undefined => { + try { + const dateQuery = createDateRangeRequest(fromField, toField, args, type); + if (dateQuery) filters.$and.push(dateQuery); + } catch (error) { + throw error; + } +}; + +export const createTabRangeRequest = (fromField: string, toField: string, type: DateTabType = DateTabType.today): any | undefined => { + let from; + let to; + switch (type) { + case DateTabType.today: + from = moment().startOf('day'); + to = moment().endOf('day'); + break; + case DateTabType.tomorrow: + from = moment() + .add(1, 'day') + .startOf('day'); + to = moment() + .add(1, 'day') + .endOf('day'); + break; + case DateTabType.upcomming: + from = moment() + .add(2, 'day') + .startOf('day'); + break; + case DateTabType.past: + to = moment().startOf('day'); + break; + default: + break; + } + + if (from && to) { + return { + $or: [{ [`${fromField}`]: { $gte: from.toDate(), $lte: to.toDate() } }, { [`${toField}`]: { $gte: from.toDate(), $lte: to.toDate() } }], + }; + } else if (from) { + return { + $or: [{ [`${fromField}`]: { $gte: from.toDate() } }, { [`${toField}`]: { $gte: from.toDate() } }], + }; + } else if (to) { + return { + $or: [{ [`${fromField}`]: { $lte: to.toDate() } }, { [`${toField}`]: { $lte: to.toDate() } }], + }; + } else return; +}; +export const addDateTabRequest = ( + fromField: string, + toField: string, + filters: { $and: any[] }, + type: DateTabType = DateTabType.today, +): any | undefined => { + try { + const dateQuery = createTabRangeRequest(fromField, toField, type); + if (dateQuery) filters.$and.push(dateQuery); + } catch (error) { + throw error; + } +}; + +export const addGeoAggregatePipeline = (longitude: number, latitude: number, radiusInMeter: number, query?: any): any | undefined => { + if (query) + return { + $geoNear: { + near: { type: 'Point', coordinates: [longitude, latitude] }, + distanceField: 'dist.calculated', + maxDistance: radiusInMeter, + query: query, + includeLocs: 'dist.location', + spherical: true, + }, + }; + + return { + $geoNear: { + near: { type: 'Point', coordinates: [longitude, latitude] }, + distanceField: 'dist.calculated', + maxDistance: radiusInMeter, + includeLocs: 'dist.location', + spherical: true, + }, + }; +}; + +export const createGeoAggreatePipeline = (input: { + collection: Collection; + longitude: number; + latitude: number; + radiusInMeter: number; + count?: boolean; + query?: any; + pagination?: { limit: number; skip: number }; +}): AggregationCursor => { + // eslint-disable-next-line prefer-const + let { collection, longitude, latitude, radiusInMeter, count, query, pagination } = input; + if (!pagination) pagination = { limit: 100, skip: 0 }; + + const pipe = addGeoAggregatePipeline(longitude, latitude, radiusInMeter, query); + if (count) { + return collection.aggregate([{ $geoNear: pipe.$geoNear }, { $count: 'count' }]); + } else { + return collection.aggregate([{ $geoNear: pipe.$geoNear }, { $skip: pagination.skip }, { $limit: pagination.limit }]); + } +}; + +export const addFilterRequest = (model: T, args: any, filters: { $and: any[] }, check = true): any | undefined => { + if (model.searchEngine) { + const searchEngine = model.searchEngine(); + for (const key in searchEngine) { + const el = searchEngine[key] as SearchEngineValue; + + const fieldName = el.dbFName || key; + + if (el.operation == 'between') { + if (!el.beetweenConfig) throw 'Bad search engine config'; + if (args[el.beetweenConfig.inputFromField] && args[el.beetweenConfig.inputToField]) + filters.$and.push({ + [`${fieldName}`]: { $gte: args[el.beetweenConfig.inputFromField], $lte: args[el.beetweenConfig.inputToField] }, + }); + else if (args[el.beetweenConfig.inputFromField]) + filters.$and.push({ [`${fieldName}`]: { $gte: args[el.beetweenConfig.inputFromField] } }); + else if (args[el.beetweenConfig.inputToField]) + filters.$and.push({ [`${fieldName}`]: { $lte: args[el.beetweenConfig.inputToField] } }); + } else if (el.operation == 'dateRange') { + if (!el.dateRangeConfig) throw 'Bad search engine config'; + + let { inputFromField, inputToField, dbFromField, dbToField, dateRangeType } = el.dateRangeConfig; + + // Check who has the args + const argsToUse = args[key] || args; + + if (argsToUse['rangeType']) dateRangeType = argsToUse['rangeType']; + + if (argsToUse[inputFromField] && argsToUse[inputToField]) { + if (dateRangeType == RangeType.strict) + if (dbFromField == dbToField) + filters.$and.push({ + [`${dbFromField}`]: { + $gte: moment(argsToUse[inputFromField]).toDate(), + $lte: moment(argsToUse[inputToField]).toDate(), + }, + }); + else + filters.$and.push({ + [`${dbFromField}`]: { $gte: moment(argsToUse[inputFromField]).toDate() }, + [`${dbToField}`]: { $lte: moment(argsToUse[inputToField]).toDate() }, + }); + else if (dateRangeType == RangeType.intersect) + filters.$and.push({ + $or: [ + { + [`${dbFromField}`]: { + $gte: moment(argsToUse[inputFromField]).toDate(), + $lte: moment(argsToUse[inputToField]).toDate(), + }, + }, + { + [`${dbToField}`]: { + $gte: moment(argsToUse[inputFromField]).toDate(), + $lte: moment(argsToUse[inputToField]).toDate(), + }, + }, + ], + }); + else if (dateRangeType == RangeType.included) { + if (dbFromField == dbToField) + filters.$and.push({ + [`${dbFromField}`]: { + $lte: moment(argsToUse[inputToField]).toDate(), + $gte: moment(argsToUse[inputFromField]).toDate(), + }, + }); + else + filters.$and.push({ + [`${dbFromField}`]: { $lte: moment(argsToUse[inputFromField]).toDate() }, + [`${dbToField}`]: { $gte: moment(argsToUse[inputToField]).toDate() }, + }); + } else + filters.$and.push({ + $or: [ + { [`${dbFromField}`]: { $gte: moment(argsToUse[inputFromField]).toDate() } }, + { [`${dbToField}`]: { $lte: moment(argsToUse[inputToField]).toDate() } }, + ], + }); + } + } else if (el.operation == 'hourRange') { + if (!el.hourRangeConfig) throw 'Bad search engine config'; + + const { inputFromField, inputToField, dbMainField, dbSubfieldFrom, dbSubfieldTo, dateRangeType } = el.hourRangeConfig; + + // Get the subfi + + if (args[inputFromField] && args[inputToField]) { + if (dateRangeType == RangeType.strict) + filters.$and.push({ + [`${dbMainField}`]: { + $elemMatch: { + [`${dbSubfieldFrom}`]: { $gte: args[inputFromField] }, + [`${dbSubfieldTo}`]: { $lte: args[inputToField] }, + }, + }, + }); + else if (dateRangeType == RangeType.intersect) + filters.$and.push({ + [`${dbMainField}`]: { + $elemMatch: { + $or: [ + { + [`${dbSubfieldFrom}`]: { + $gte: args[inputFromField], + $lte: args[inputToField], + }, + }, + { [`${dbSubfieldTo}`]: { $gte: args[inputFromField], $lte: args[inputToField] } }, + ], + }, + }, + }); + else if (dateRangeType == RangeType.included) + filters.$and.push({ + [`${dbMainField}`]: { + $elemMatch: { + [`${dbSubfieldFrom}`]: { $lte: args[inputFromField] }, + [`${dbSubfieldTo}`]: { $gte: args[inputToField] }, + }, + }, + }); + else + filters.$and.push({ + $elemMatch: { + $or: [ + { [`${dbSubfieldFrom}`]: { $gte: args[inputFromField] } }, + { [`${dbSubfieldTo}`]: { $lte: args[inputToField] } }, + ], + }, + }); + } + } else if (el.operation == 'dateTab') { + if (!el.dateTabConfig) throw 'Bad search engine config'; + const { dbFromField, dbToField } = el.dateTabConfig; + if (args.dateTabType) addDateTabRequest(dbFromField, dbToField, filters, args.dateTabType); + } else if (el.operation == 'bool') { + if (args[key] === false || args[key] === true) { + filters.$and.push({ [`${fieldName}`]: args[key] }); + } + } else if (el.operation == 'nameRegex') { + if (args[key]) { + const searchValue = escapeRegex(args[key]); + filters.$and.push({ + $expr: { + $regexMatch: { + input: { $concat: ['$firstName', ' ', '$lastName'] }, + regex: `.*${searchValue}.*`, + options: 'i', + }, + }, + }); + } + } else if (el.operation == 'regex') { + if (args[key]) { + const searchValue = args[key].replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + filters.$and.push({ [`${fieldName}`]: { $regex: `.*${searchValue}.*`, $options: 'i' } }); + } + } else if (el.operation == 'freeInput') { + if (!el.freeInputConfig) throw 'Bad search engine config'; + const freeInputValue = args[key]; + + if (freeInputValue) { + const searchValue = escapeRegex(freeInputValue); + + const searchOptions = el.freeInputConfig.searchOptions; + if (searchOptions.length > 0) { + const $or: any[] = []; + for (let index = 0; index < searchOptions.length; index++) { + const element = searchOptions[index]; + $or.push({ [`${element}`]: { $regex: `.*${searchValue}.*`, $options: 'i' } }); + } + filters.$and.push({ $or }); + } + } + } else if (el.operation == 'searchText') { + if (args[key]) { + const searchTextValue = escapeRegex(args[key]); + // if (searchTextValue) filters.$and.push({ $text: { $search: searchTextValue } }); + if (searchTextValue) filters.$and.push({ searchT: { $regex: `.*${searchTextValue}.*`, $options: 'i' } }); + } + } else { + if (el.forcedValue != undefined) filters.$and.push({ [`${fieldName}`]: { [`${el.operation}`]: el.forcedValue } }); + else if (args[key] != undefined) filters.$and.push({ [`${fieldName}`]: { [`${el.operation}`]: args[key] } }); + } + } + } else { + const filterOptions = model.filterOptions ? model.filterOptions() : null; + delete args.search; + + for (const key in args) { + const element = args[key]; + + if (check) { + if (filterOptions && filterOptions.includes(key)) { + if (key == '_ids') { + filters.$and.push({ _id: { $in: (element as string).replace(' ', '').split(',') } }); + } else filters.$and.push({ [`${key}`]: element }); + } + } else { + if (key == '_ids') { + filters.$and.push({ _id: { $in: (element as string).replace(' ', '').split(',') } }); + } else filters.$and.push({ [`${key}`]: element }); + } + } + } + + return; +}; + +export const baseSearchFunction = async (input: { + model: T; + query?: any; + count?: boolean; + pagination?: GetArgs; + all?: boolean; + engine?: boolean; + ctx: ApolloContext | null; +}): Promise => { + const { model, query, pagination, all, ctx } = input; + + let finalSearch: any = { $and: [] }; + let gSearch: GeolocSearchComponent | undefined; + + let count = false; + + if (query) { + const { search, geoSearch, geoSearchWithAddress, /*geoPlaceSearch,*/ afterCreatedAt, afterUpdatedAt, _ids, n_ids, ...filterArgument } = query; + + if (input.count) count = true; + + if (search) { + const $or = createSearchRequest(model, query); + if ($or) finalSearch.$and.push({ $or }); + } + + if (geoSearch) gSearch = geoSearch; + else if (geoSearchWithAddress) { + const geoloc = await getGeoLocFromAddress(geoSearchWithAddress.formattedAddress); + gSearch = { + longitude: geoloc.loc.coordinates[0], + latitude: geoloc.loc.coordinates[1], + radius: geoSearchWithAddress.radius, + }; + } + + if (filterArgument) { + addFilterRequest(model, { search, ...filterArgument }, finalSearch, false); + } + + if (_ids) finalSearch.$and.push({ _id: { $in: _ids } }); + // eslint-disable-next-line @typescript-eslint/camelcase + if (n_ids) finalSearch.$and.push({ _id: { $nin: n_ids } }); + + if (afterCreatedAt) finalSearch.$and.push({ createdAt: { $gte: new Date(afterCreatedAt) } }); + if (afterUpdatedAt) finalSearch.$and.push({ updatedAt: { $gte: new Date(afterUpdatedAt) } }); + } + + // if (query.search) finalSearch.$and.push({ search: { $regex: `.*${query.search}.*`, $options: 'i' } }); + + // Check if exist in DB + if (finalSearch.$and.length == 0) finalSearch = {}; + + try { + if (gSearch) { + let pipelineCursor; + const finalQ = model.getQuery(finalSearch, ctx); + + if (count) { + pipelineCursor = createGeoAggreatePipeline({ + ...gSearch, + collection: await model.db(), + query: finalQ, + radiusInMeter: gSearch.radius, + count: true, + }); + } else { + const paginationOption = parsePaginationOptions(pagination); + pipelineCursor = createGeoAggreatePipeline({ + ...gSearch, + collection: await model.db(), + query: finalQ, + radiusInMeter: gSearch.radius, + pagination: paginationOption, + }); + } + + if (input.engine) { + const result = await pipelineCursor.toArray(); + if (count) return result[0] || 0; + else return ((model as unknown) as EngineModel).plainToClass(result); + } else { + const result = await pipelineCursor.toArray(); + if (count) return result[0] || 0; + else return result; + } + } else { + if (input.engine) { + if (count) { + console.log('counting'); + return await ((model as unknown) as EngineModel).getCount({ query: finalSearch, ctx }); + } else { + if (all) return await ((model as unknown) as EngineModel).getAll({ query: finalSearch, ctx }); + return await ((model as unknown) as EngineModel).getMany({ query: finalSearch, pagination, ctx }); + } + } else { + if (count) { + console.log('counting'); + return await model.getCount(finalSearch, ctx); + } else { + if (all) return await model.getAll(finalSearch, ctx); + return await model.getMany(finalSearch, pagination, ctx); + } + } + } + } catch (error) { + throw error; + } +}; diff --git a/lib/seed/services/database/DBService.ts b/lib/seed/services/database/DBService.ts new file mode 100644 index 0000000..73874d1 --- /dev/null +++ b/lib/seed/services/database/DBService.ts @@ -0,0 +1,61 @@ +import { MongoClient, Db } from 'mongodb'; +import _, { difference } from 'lodash'; +import { differenceBetweenObject } from '@seed/helpers/Utils'; + +export const createSetRequest = (field: string, data: object, oldObject?: any): Partial => { + let dataToChange; + if (oldObject) dataToChange = differenceBetweenObject(data, oldObject); + else dataToChange = data; + + const req: any = {}; + for (const key in dataToChange) { + if (dataToChange.hasOwnProperty(key)) { + if (_.isPlainObject(dataToChange[key])) { + if (field != '') req[`${field}.${key}`] = createSetRequest(key, dataToChange[key]); + else req[`${key}`] = createSetRequest('', dataToChange[key]); + } else { + if (field != '') req[`${field}.${key}`] = dataToChange[key]; + else req[`${key}`] = dataToChange[key]; + } + } + } + + return req; +}; + +export default class DB { + private static instance: DB; + public mongoClient!: MongoClient; + public db!: Db; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + public static async getInstance(): Promise { + if (!this.instance || !this.instance.mongoClient || !this.instance.mongoClient.isConnected) { + this.instance = new DB(); + + let mongoUrl; + if (process.env.MONGO_USER && process.env.MONGO_PWD) + mongoUrl = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PWD}@${process.env.MONGO_URL}`; + else mongoUrl = process.env.MONGO_URL || ''; + const mongoDb = process.env.MONGO_DB || ''; + + try { + this.instance.mongoClient = await MongoClient.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + // loggerLevel: 'debug', + }); + this.instance.db = this.instance.mongoClient.db(mongoDb); + if (process.env.NODE_ENV != 'production') console.log('[MongoDB] New Connect'); + } catch (error) { + throw error; + } + } else { + if (process.env.NODE_ENV != 'production') console.log('[MongoDB] Existing Connect'); + } + + return DB.instance; + } +} diff --git a/lib/seed/services/database/FirestoreService.ts b/lib/seed/services/database/FirestoreService.ts new file mode 100644 index 0000000..82d4454 --- /dev/null +++ b/lib/seed/services/database/FirestoreService.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import FirebaseAdmin from 'firebase-admin'; + +export interface FirebaseUser { + aud: string; + auth_time: number; + email: string | undefined; + email_verified: boolean | undefined; + exp: number; + firebase: { + identities: any; + sign_in_provider: 'custom' | 'password' | 'phone' | 'anonymous' | 'google.com' | 'facebook.com' | 'github.com' | 'twitter.com'; + }; + iat: number; + name: string; + picture: string | undefined; + uid: string; +} + +export default class Firestore { + private static instance: any; + public static db: FirebaseFirestore.Firestore; + + private constructor() { + try { + const base = { + type: 'service_account', + project_id: process.env.FIRESTORE_project_id || '', + private_key_id: process.env.FIRESTORE_private_key_id || '', + private_key: process.env.FIRESTORE_private_key ? process.env.FIRESTORE_private_key.replace(/\\n/g, '\n') : '', + client_email: process.env.FIRESTORE_client_email || '', + client_id: process.env.FIRESTORE_client_id || '', + auth_uri: process.env.FIRESTORE_auth_uri || '', + token_uri: process.env.FIRESTORE_token_uri || '', + auth_provider_x509_cert_url: process.env.FIRESTORE_auth_provider_x509_cert_url || '', + client_x509_cert_url: process.env.FIRESTORE_client_x509_cert_url || '', + apiKey: process.env.FIRESTORE_apiKey || '', + }; + + FirebaseAdmin.initializeApp( + { + credential: FirebaseAdmin.credential.cert({ + projectId: base.project_id, + clientEmail: base.client_email, + privateKey: base.private_key, + }), + }, + 'firestore', + ); + + Firestore.instance = FirebaseAdmin; + Firestore.db = FirebaseAdmin.firestore(); + } catch (error) { + if (!/already exists/.test(error.message)) { + console.error('Firebase initialization error raised', error.stack); + } + } + } + + public static getInstance(): FirebaseFirestore.Firestore { + if (!Firestore.instance) { + Firestore.instance = new Firestore(); + } + + return Firestore.db; + } +} diff --git a/lib/seed/services/database/LoaderService.ts b/lib/seed/services/database/LoaderService.ts new file mode 100644 index 0000000..cfaac0d --- /dev/null +++ b/lib/seed/services/database/LoaderService.ts @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import _ from 'lodash'; +import DL from 'dataloader'; +import { EngineModel } from '@seed/engine/EngineModel'; + +export class DataLoader extends DL { + async loadMany(keys: ArrayLike): Promise> { + const nkeys = _.without(keys, undefined, null) as any; + const res = await super.loadMany(nkeys); + return _.without(res, undefined, null) as any; + } +} + +/* V1 - Graph Model */ + +export const batchFunction = async (model: T, _ids: any, field: string) => { + const results = await (await (model as any).db()).find({ [`${field}`]: { $in: _ids } }).toArray(); + return _ids.map((_id) => results.find((result) => result[field] === _id)); +}; + +export const batchOneToManyFunction = async (model: T, _ids: any, field: string) => { + const results = await (await (model as any).db()).find({ [`${field}`]: { $in: _ids } }).toArray(); + return _ids.map((_id) => _.filter(results, { [`${field}`]: _id })); +}; + +export const batchOneToManyFilterFunction = async (model: T, _ids: any, field: string, filter: any) => { + const results = await (await (model as any).db()).find({ [`${field}`]: { $in: _ids }, ...filter }).toArray(); + return _ids.map((_id) => _.filter(results, { [`${field}`]: _id })); +}; + +export const buildLoader = (model: T, field = '_id') => { + return new DataLoader((keys) => { + return batchFunction(model, keys, field); + }); +}; + +export const buildOneToManyLoader = (model: T, field = '_id') => { + return new DataLoader>((keys) => { + return batchOneToManyFunction(model, keys, field); + }); +}; + +export const buildOneToManyFilterLoader = (model: T, field = '_id', filter: any) => { + return new DataLoader>((keys) => { + return batchOneToManyFilterFunction(model, keys, field, filter); + }); +}; + +/* V2 - Engine Model */ +export const batchEngineFunction = async >(model: T, _ids: any, field: string) => { + const results = await model.getAll({ query: { [`${field}`]: { $in: _ids } } }); + return _ids.map((_id) => results.find((result) => result[field] === _id)); +}; + +export const buildEngineLoader = , V = T>(model: T, field = '_id') => { + return new DataLoader((keys) => { + return batchEngineFunction(model, keys, field); + }); +}; diff --git a/lib/seed/services/email/EmailHelper.ts b/lib/seed/services/email/EmailHelper.ts new file mode 100644 index 0000000..1d003ca --- /dev/null +++ b/lib/seed/services/email/EmailHelper.ts @@ -0,0 +1,118 @@ +import { interpolate } from '@seed/helpers/Utils'; +import EmailSettingsModel from '@services/module-cms/functions/emails-settings/emails-settings.model'; +import { AvailableTranslation } from '@src/__components/components'; + +export const sendEmailsHelper = async ( + emailSettings: EmailSettingsModel, + finalLanguage: AvailableTranslation, + emails: string | string[], + data: any, +): Promise => { + const templateErrorReportingEmail = process.env.MJ_ERROR_EMAIL || 'software@makeit-studio.com'; + + const toEmails: any[] = []; + + if (!Array.isArray(emails)) emails = [emails]; + for (let index = 0; index < emails.length; index++) { + toEmails.push({ Email: emails[index] }); + } + + const ccEmails: any[] = []; + if (emailSettings.cci && emailSettings.cci.length > 0) + for (let index = 0; index < emailSettings.cci.length; index++) { + ccEmails.push({ Email: emailSettings.cci[index] }); + } + + const body = emailSettings.body && emailSettings.body[finalLanguage] ? emailSettings.body[finalLanguage] : ''; + const subject = emailSettings.subject && emailSettings.subject[finalLanguage] ? emailSettings.subject[finalLanguage] : ''; + const templateId = + emailSettings.templateId && emailSettings.templateId[finalLanguage] ? emailSettings.templateId[finalLanguage]!.replace(' ', '') : ''; + let messages; + if (!emailSettings.custom && emailSettings.subject && emailSettings.body) { + // Check if valide template + + // If no template -> Send only what there is in DB + if (!templateId || templateId == '') { + console.log('MJ.LOG', 'sending no template'); + + messages = [ + { + From: { + Email: emailSettings.fromEmail, + Name: emailSettings.fromName, + }, + To: toEmails, + Cc: ccEmails, + ReplyTo: { + Email: emailSettings.replyToEmail, + }, + HTMLPart: interpolate(body || '', data, { withEol: true }), + Subject: interpolate(subject || '', data, { withEol: true }), + }, + ]; + } + // If template -> Send with template ID + else { + console.log('MJ.LOG', 'sending template body'); + messages = [ + { + From: { + Email: emailSettings.fromEmail, + Name: emailSettings.fromName, + }, + To: toEmails, + Cc: ccEmails, + ReplyTo: { + Email: emailSettings.replyToEmail, + }, + TemplateID: parseInt(templateId), + TemplateLanguage: true, + TemplateErrorReporting: { + Email: templateErrorReportingEmail, + }, + Subject: interpolate(subject || '', data, { withEol: true }), + Variables: { + body: interpolate(body || '', data, { withEol: true }), + }, + }, + ]; + } + } else { + console.log('MJ.LOG', 'sending template data'); + + messages = [ + { + From: { + Email: emailSettings.fromEmail, + Name: emailSettings.fromName, + }, + To: toEmails, + Cc: ccEmails, + ReplyTo: { + Email: emailSettings.replyToEmail, + }, + TemplateID: parseInt(templateId), + TemplateLanguage: true, + TemplateErrorReporting: { + Email: templateErrorReportingEmail, + }, + Variables: data, + }, + ]; + } + + const mailjet = require('node-mailjet').connect(process.env.MJ_APIKEY_PUBLIC, process.env.MJ_APIKEY_PRIVATE); + try { + // eslint-disable-next-line @typescript-eslint/camelcase + const res = await mailjet.post('send', { version: 'v3.1', perform_api_call: true }).request({ + Messages: messages, + }); + console.log('MJ.SUCCESS', emailSettings.key, res.response.text); + + return true; + } catch (error) { + console.error('MJ.ERROR', emailSettings.key, error); + console.error('MJ MESSAGE', messages); + return false; + } +}; diff --git a/lib/seed/services/email/EmailService.ts b/lib/seed/services/email/EmailService.ts new file mode 100644 index 0000000..c8ceaaf --- /dev/null +++ b/lib/seed/services/email/EmailService.ts @@ -0,0 +1,20 @@ +import { AvailableTranslation } from '@src/__components/components'; +import EmailSettingsModel from '@services/module-cms/functions/emails-settings/emails-settings.model'; +import { sendEmailsHelper } from './EmailHelper'; + +const templateErrorReportingEmail = process.env.MJ_ERROR_EMAIL || 'software@makeit-studio.com'; + +export const sendEmail = async (key: string, language: AvailableTranslation | null, email: string[] | string, data: T): Promise => { + const emailSettings = new EmailSettingsModel(); + + const finalLanguage = language || (process.env.DEFAULT_LANGUAGE as any) || AvailableTranslation.en; + + try { + await emailSettings.getOne({ key }, null); + } catch (error) { + console.error('EMAIL.ERROR', { message: 'notfound', key }); + return false; + } + + return await sendEmailsHelper(emailSettings, finalLanguage as any, email, data); +}; diff --git a/lib/seed/services/geolocation/README.md b/lib/seed/services/geolocation/README.md new file mode 100644 index 0000000..6a92f2e --- /dev/null +++ b/lib/seed/services/geolocation/README.md @@ -0,0 +1,9 @@ +# Make-it Geoloc Module + +[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com) + +![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) + + + +npm install @googlemaps/google-maps-services-js geolib diff --git a/lib/seed/services/geolocation/__tests/geolocation.playground.gql b/lib/seed/services/geolocation/__tests/geolocation.playground.gql new file mode 100644 index 0000000..e69de29 diff --git a/lib/seed/services/geolocation/components/countries.ts b/lib/seed/services/geolocation/components/countries.ts new file mode 100644 index 0000000..eb9b4ee --- /dev/null +++ b/lib/seed/services/geolocation/components/countries.ts @@ -0,0 +1,514 @@ +import { registerEnumType } from 'type-graphql'; + +export enum CountryCodesComponentEnum { + 'AF' = 'AF', + 'AX' = 'AX', + 'AL' = 'AL', + 'DZ' = 'DZ', + 'AS' = 'AS', + 'AD' = 'AD', + 'AO' = 'AO', + 'AI' = 'AI', + 'AQ' = 'AQ', + 'AG' = 'AG', + 'AR' = 'AR', + 'AM' = 'AM', + 'AW' = 'AW', + 'AU' = 'AU', + 'AT' = 'AT', + 'AZ' = 'AZ', + 'BS' = 'BS', + 'BH' = 'BH', + 'BD' = 'BD', + 'BB' = 'BB', + 'BY' = 'BY', + 'BE' = 'BE', + 'BZ' = 'BZ', + 'BJ' = 'BJ', + 'BM' = 'BM', + 'BT' = 'BT', + 'BO' = 'BO', + 'BA' = 'BA', + 'BW' = 'BW', + 'BV' = 'BV', + 'BR' = 'BR', + 'IO' = 'IO', + 'BN' = 'BN', + 'BG' = 'BG', + 'BF' = 'BF', + 'BI' = 'BI', + 'KH' = 'KH', + 'CM' = 'CM', + 'CA' = 'CA', + 'CV' = 'CV', + 'KY' = 'KY', + 'CF' = 'CF', + 'TD' = 'TD', + 'CL' = 'CL', + 'CN' = 'CN', + 'CX' = 'CX', + 'CC' = 'CC', + 'CO' = 'CO', + 'KM' = 'KM', + 'CG' = 'CG', + 'CD' = 'CD', + 'CK' = 'CK', + 'CR' = 'CR', + 'CI' = 'CI', + 'HR' = 'HR', + 'CU' = 'CU', + 'CY' = 'CY', + 'CZ' = 'CZ', + 'DK' = 'DK', + 'DJ' = 'DJ', + 'DM' = 'DM', + 'DO' = 'DO', + 'EC' = 'EC', + 'EG' = 'EG', + 'SV' = 'SV', + 'GQ' = 'GQ', + 'ER' = 'ER', + 'EE' = 'EE', + 'ET' = 'ET', + 'FK' = 'FK', + 'FO' = 'FO', + 'FJ' = 'FJ', + 'FI' = 'FI', + 'FR' = 'FR', + 'GF' = 'GF', + 'PF' = 'PF', + 'TF' = 'TF', + 'GA' = 'GA', + 'GM' = 'GM', + 'GE' = 'GE', + 'DE' = 'DE', + 'GH' = 'GH', + 'GI' = 'GI', + 'GR' = 'GR', + 'GL' = 'GL', + 'GD' = 'GD', + 'GP' = 'GP', + 'GU' = 'GU', + 'GT' = 'GT', + 'GG' = 'GG', + 'GN' = 'GN', + 'GW' = 'GW', + 'GY' = 'GY', + 'HT' = 'HT', + 'HM' = 'HM', + 'VA' = 'VA', + 'HN' = 'HN', + 'HK' = 'HK', + 'HU' = 'HU', + 'IS' = 'IS', + 'IN' = 'IN', + 'ID' = 'ID', + 'IR' = 'IR', + 'IQ' = 'IQ', + 'IE' = 'IE', + 'IM' = 'IM', + 'IL' = 'IL', + 'IT' = 'IT', + 'JM' = 'JM', + 'JP' = 'JP', + 'JE' = 'JE', + 'JO' = 'JO', + 'KZ' = 'KZ', + 'KE' = 'KE', + 'KI' = 'KI', + 'KR' = 'KR', + 'KW' = 'KW', + 'KG' = 'KG', + 'LA' = 'LA', + 'LV' = 'LV', + 'LB' = 'LB', + 'LS' = 'LS', + 'LR' = 'LR', + 'LY' = 'LY', + 'LI' = 'LI', + 'LT' = 'LT', + 'LU' = 'LU', + 'MO' = 'MO', + 'MK' = 'MK', + 'MG' = 'MG', + 'MW' = 'MW', + 'MY' = 'MY', + 'MV' = 'MV', + 'ML' = 'ML', + 'MT' = 'MT', + 'MH' = 'MH', + 'MQ' = 'MQ', + 'MR' = 'MR', + 'MU' = 'MU', + 'YT' = 'YT', + 'MX' = 'MX', + 'FM' = 'FM', + 'MD' = 'MD', + 'MC' = 'MC', + 'MN' = 'MN', + 'ME' = 'ME', + 'MS' = 'MS', + 'MA' = 'MA', + 'MZ' = 'MZ', + 'MM' = 'MM', + 'NA' = 'NA', + 'NR' = 'NR', + 'NP' = 'NP', + 'NL' = 'NL', + 'AN' = 'AN', + 'NC' = 'NC', + 'NZ' = 'NZ', + 'NI' = 'NI', + 'NE' = 'NE', + 'NG' = 'NG', + 'NU' = 'NU', + 'NF' = 'NF', + 'MP' = 'MP', + 'NO' = 'NO', + 'OM' = 'OM', + 'PK' = 'PK', + 'PW' = 'PW', + 'PS' = 'PS', + 'PA' = 'PA', + 'PG' = 'PG', + 'PY' = 'PY', + 'PE' = 'PE', + 'PH' = 'PH', + 'PN' = 'PN', + 'PL' = 'PL', + 'PT' = 'PT', + 'PR' = 'PR', + 'QA' = 'QA', + 'RE' = 'RE', + 'RO' = 'RO', + 'RU' = 'RU', + 'RW' = 'RW', + 'BL' = 'BL', + 'SH' = 'SH', + 'KN' = 'KN', + 'LC' = 'LC', + 'MF' = 'MF', + 'PM' = 'PM', + 'VC' = 'VC', + 'WS' = 'WS', + 'SM' = 'SM', + 'ST' = 'ST', + 'SA' = 'SA', + 'SN' = 'SN', + 'RS' = 'RS', + 'SC' = 'SC', + 'SL' = 'SL', + 'SG' = 'SG', + 'SK' = 'SK', + 'SI' = 'SI', + 'SB' = 'SB', + 'SO' = 'SO', + 'ZA' = 'ZA', + 'GS' = 'GS', + 'ES' = 'ES', + 'LK' = 'LK', + 'SD' = 'SD', + 'SR' = 'SR', + 'SJ' = 'SJ', + 'SZ' = 'SZ', + 'SE' = 'SE', + 'CH' = 'CH', + 'SY' = 'SY', + 'TW' = 'TW', + 'TJ' = 'TJ', + 'TZ' = 'TZ', + 'TH' = 'TH', + 'TL' = 'TL', + 'TG' = 'TG', + 'TK' = 'TK', + 'TO' = 'TO', + 'TT' = 'TT', + 'TN' = 'TN', + 'TR' = 'TR', + 'TM' = 'TM', + 'TC' = 'TC', + 'TV' = 'TV', + 'UG' = 'UG', + 'UA' = 'UA', + 'AE' = 'AE', + 'GB' = 'GB', + 'US' = 'US', + 'UM' = 'UM', + 'UY' = 'UY', + 'UZ' = 'UZ', + 'VU' = 'VU', + 'VE' = 'VE', + 'VN' = 'VN', + 'VG' = 'VG', + 'VI' = 'VI', + 'WF' = 'WF', + 'EH' = 'EH', + 'YE' = 'YE', + 'ZM' = 'ZM', + 'ZW' = 'ZW', + 'XK' = 'XK', + 'KP' = 'KP', + 'SS' = 'SS', + 'SX' = 'SX', + 'BQ' = 'BQ', + 'CW' = 'CW', +} +registerEnumType(CountryCodesComponentEnum, { + name: 'CountryCodesComponentEnum', +}); + +export const isoCountries = { + AF: 'Afghanistan', + AX: 'Aland Islands', + AL: 'Albania', + DZ: 'Algeria', + AS: 'American Samoa', + AD: 'Andorra', + AO: 'Angola', + AI: 'Anguilla', + AQ: 'Antarctica', + AG: 'Antigua And Barbuda', + AR: 'Argentina', + AM: 'Armenia', + AW: 'Aruba', + AU: 'Australia', + AT: 'Austria', + AZ: 'Azerbaijan', + BS: 'Bahamas', + BH: 'Bahrain', + BD: 'Bangladesh', + BB: 'Barbados', + BY: 'Belarus', + BE: 'Belgium', + BZ: 'Belize', + BJ: 'Benin', + BM: 'Bermuda', + BT: 'Bhutan', + BO: 'Bolivia', + BA: 'Bosnia And Herzegovina', + BW: 'Botswana', + BV: 'Bouvet Island', + BR: 'Brazil', + IO: 'British Indian Ocean Territory', + BN: 'Brunei Darussalam', + BG: 'Bulgaria', + BF: 'Burkina Faso', + BI: 'Burundi', + KH: 'Cambodia', + CM: 'Cameroon', + CA: 'Canada', + CV: 'Cape Verde', + KY: 'Cayman Islands', + CF: 'Central African Republic', + TD: 'Chad', + CL: 'Chile', + CN: 'China', + CX: 'Christmas Island', + CC: 'Cocos (Keeling) Islands', + CO: 'Colombia', + KM: 'Comoros', + CG: 'Congo', + CD: 'Congo, Democratic Republic', + CK: 'Cook Islands', + CR: 'Costa Rica', + CI: "Cote D'Ivoire", + HR: 'Croatia', + CU: 'Cuba', + CY: 'Cyprus', + CZ: 'Czech Republic', + DK: 'Denmark', + DJ: 'Djibouti', + DM: 'Dominica', + DO: 'Dominican Republic', + EC: 'Ecuador', + EG: 'Egypt', + SV: 'El Salvador', + GQ: 'Equatorial Guinea', + ER: 'Eritrea', + EE: 'Estonia', + ET: 'Ethiopia', + FK: 'Falkland Islands (Malvinas)', + FO: 'Faroe Islands', + FJ: 'Fiji', + FI: 'Finland', + FR: 'France', + GF: 'French Guiana', + PF: 'French Polynesia', + TF: 'French Southern Territories', + GA: 'Gabon', + GM: 'Gambia', + GE: 'Georgia', + DE: 'Germany', + GH: 'Ghana', + GI: 'Gibraltar', + GR: 'Greece', + GL: 'Greenland', + GD: 'Grenada', + GP: 'Guadeloupe', + GU: 'Guam', + GT: 'Guatemala', + GG: 'Guernsey', + GN: 'Guinea', + GW: 'Guinea-Bissau', + GY: 'Guyana', + HT: 'Haiti', + HM: 'Heard Island & Mcdonald Islands', + VA: 'Holy See (Vatican City State)', + HN: 'Honduras', + HK: 'Hong Kong', + HU: 'Hungary', + IS: 'Iceland', + IN: 'India', + ID: 'Indonesia', + IR: 'Iran, Islamic Republic Of', + IQ: 'Iraq', + IE: 'Ireland', + IM: 'Isle Of Man', + IL: 'Israel', + IT: 'Italy', + JM: 'Jamaica', + JP: 'Japan', + JE: 'Jersey', + JO: 'Jordan', + KZ: 'Kazakhstan', + KE: 'Kenya', + KI: 'Kiribati', + KR: 'Korea', + KW: 'Kuwait', + KG: 'Kyrgyzstan', + LA: "Lao People's Democratic Republic", + LV: 'Latvia', + LB: 'Lebanon', + LS: 'Lesotho', + LR: 'Liberia', + LY: 'Libyan Arab Jamahiriya', + LI: 'Liechtenstein', + LT: 'Lithuania', + LU: 'Luxembourg', + MO: 'Macao', + MK: 'Macedonia', + MG: 'Madagascar', + MW: 'Malawi', + MY: 'Malaysia', + MV: 'Maldives', + ML: 'Mali', + MT: 'Malta', + MH: 'Marshall Islands', + MQ: 'Martinique', + MR: 'Mauritania', + MU: 'Mauritius', + YT: 'Mayotte', + MX: 'Mexico', + FM: 'Micronesia, Federated States Of', + MD: 'Moldova', + MC: 'Monaco', + MN: 'Mongolia', + ME: 'Montenegro', + MS: 'Montserrat', + MA: 'Morocco', + MZ: 'Mozambique', + MM: 'Myanmar', + NA: 'Namibia', + NR: 'Nauru', + NP: 'Nepal', + NL: 'Netherlands', + AN: 'Netherlands Antilles', + NC: 'New Caledonia', + NZ: 'New Zealand', + NI: 'Nicaragua', + NE: 'Niger', + NG: 'Nigeria', + NU: 'Niue', + NF: 'Norfolk Island', + MP: 'Northern Mariana Islands', + NO: 'Norway', + OM: 'Oman', + PK: 'Pakistan', + PW: 'Palau', + PS: 'Palestinian Territory, Occupied', + PA: 'Panama', + PG: 'Papua New Guinea', + PY: 'Paraguay', + PE: 'Peru', + PH: 'Philippines', + PN: 'Pitcairn', + PL: 'Poland', + PT: 'Portugal', + PR: 'Puerto Rico', + QA: 'Qatar', + RE: 'Reunion', + RO: 'Romania', + RU: 'Russian Federation', + RW: 'Rwanda', + BL: 'Saint Barthelemy', + SH: 'Saint Helena', + KN: 'Saint Kitts And Nevis', + LC: 'Saint Lucia', + MF: 'Saint Martin', + PM: 'Saint Pierre And Miquelon', + VC: 'Saint Vincent And Grenadines', + WS: 'Samoa', + SM: 'San Marino', + ST: 'Sao Tome And Principe', + SA: 'Saudi Arabia', + SN: 'Senegal', + RS: 'Serbia', + SC: 'Seychelles', + SL: 'Sierra Leone', + SG: 'Singapore', + SK: 'Slovakia', + SI: 'Slovenia', + SB: 'Solomon Islands', + SO: 'Somalia', + ZA: 'South Africa', + GS: 'South Georgia And Sandwich Isl.', + ES: 'Spain', + LK: 'Sri Lanka', + SD: 'Sudan', + SR: 'Suriname', + SJ: 'Svalbard And Jan Mayen', + SZ: 'Swaziland', + SE: 'Sweden', + CH: 'Switzerland', + SY: 'Syrian Arab Republic', + TW: 'Taiwan', + TJ: 'Tajikistan', + TZ: 'Tanzania', + TH: 'Thailand', + TL: 'Timor-Leste', + TG: 'Togo', + TK: 'Tokelau', + TO: 'Tonga', + TT: 'Trinidad And Tobago', + TN: 'Tunisia', + TR: 'Turkey', + TM: 'Turkmenistan', + TC: 'Turks And Caicos Islands', + TV: 'Tuvalu', + UG: 'Uganda', + UA: 'Ukraine', + AE: 'United Arab Emirates', + GB: 'United Kingdom', + US: 'United States', + UM: 'United States Outlying Islands', + UY: 'Uruguay', + UZ: 'Uzbekistan', + VU: 'Vanuatu', + VE: 'Venezuela', + VN: 'Viet Nam', + VG: 'Virgin Islands, British', + VI: 'Virgin Islands, U.S.', + WF: 'Wallis And Futuna', + EH: 'Western Sahara', + YE: 'Yemen', + ZM: 'Zambia', + ZW: 'Zimbabwe', +}; + +export const getCountryName = (countryCode: CountryCodesComponentEnum): string => { + if (isoCountries.hasOwnProperty(countryCode)) { + return isoCountries[countryCode]; + } else { + return countryCode; + } +}; diff --git a/lib/seed/services/geolocation/functions/GooglePlaceResolver.ts b/lib/seed/services/geolocation/functions/GooglePlaceResolver.ts new file mode 100644 index 0000000..df6af4d --- /dev/null +++ b/lib/seed/services/geolocation/functions/GooglePlaceResolver.ts @@ -0,0 +1,59 @@ +import { Resolver, Query, Arg, Mutation, Ctx, Args, Authorized, Field, ArgsType } from 'type-graphql'; + +import { ApolloContext } from '@seed/interfaces/context'; + +import { GraphQLJSONObject } from 'graphql-type-json'; +import { MinLength } from 'class-validator'; +import { AvailableTranslation } from '@src/__components/components'; +import { CountryCodesComponentEnum } from '../components/countries'; +import { autocompleteService, getGeoLocFromAddress, getPlaceDetail } from '../services/GooglePlaceService'; + +@ArgsType() +export class AutocompleteArgs { + @Field() + session: string; + + @Field() + @MinLength(5) + input: string; + + @Field(() => AvailableTranslation, { nullable: true }) + language?: AvailableTranslation; + @Field(() => [CountryCodesComponentEnum], { nullable: true }) + country?: CountryCodesComponentEnum[]; +} + +@ArgsType() +export class GeolocFromLocationIdInput { + @Field() + session: string; + @Field() + placeId: string; +} + +@Resolver() +export default class GooglePlaceResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => [GraphQLJSONObject]) + async placesAutocomplete(@Args() args: AutocompleteArgs, @Ctx() ctx: ApolloContext): Promise { + return await autocompleteService(args.session, args.input, args.language, args.country); + } + + @Query(() => GraphQLJSONObject) + async placesGeocode(@Args() args: GeolocFromLocationIdInput, @Ctx() ctx: ApolloContext): Promise { + return await getPlaceDetail(args.session, args.placeId); + } + + @Query(() => GraphQLJSONObject) + async placesGeocodeFromAddress(@Arg('address') address: string, @Ctx() ctx: ApolloContext): Promise { + return await getGeoLocFromAddress(address); + } +} diff --git a/lib/seed/services/geolocation/models/BaseGeoSeoModel.ts b/lib/seed/services/geolocation/models/BaseGeoSeoModel.ts new file mode 100644 index 0000000..abd4672 --- /dev/null +++ b/lib/seed/services/geolocation/models/BaseGeoSeoModel.ts @@ -0,0 +1,66 @@ +import { ObjectType, Field, ArgsType, InputType, Args } from 'type-graphql'; + +import { GetManyArgs } from '@seed/graphql/Request'; + + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import BaseSEOModel, { EditBaseSEOInput, NewBaseSEOInput } from '@seed/graphql/baseModels/BaseSeoModel'; +import { getDistanceBetween } from '../services/GeolocService'; +import { GeolocDistComponent, GeolocSearchComponent, PlaceComponent } from '@seed/interfaces/components.geo'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class BaseGeoSEOModel extends BaseSEOModel { + public constructor(collectionName?: string, perm?: Permission) { + const cName = collectionName ? collectionName : 'articles'; + const permission = perm ? perm : permissions; + super(cName, permission); + } + + @Field(() => PlaceComponent) + place: PlaceComponent; + + @Field(() => GeolocDistComponent, { nullable: true }) + dist?: GeolocDistComponent; + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + @Field(() => Number, { nullable: true }) + async getDistFromLocation(@Args() location: GeolocSearchComponent) { + return getDistanceBetween( + { + latitude: this.place.loc.coordinates[1], + longitude: this.place.loc.coordinates[0], + }, + location, + ); + } + + searchOptions(): string[] { + return []; + } + filterOptions(): string[] { + return []; + } +} + +@ArgsType() +export class BaseSEOArgs extends GetManyArgs {} + +@InputType() +export class NewGeoBaseSEOInput extends NewBaseSEOInput implements Partial { + @Field(() => PlaceComponent) + place: PlaceComponent; +} + +@InputType() +export class EditGeoBaseSEOInput extends EditBaseSEOInput implements Partial { + @Field(() => PlaceComponent, { nullable: true }) + place?: PlaceComponent; +} diff --git a/lib/seed/services/geolocation/models/BaseGeoSimpleModel.ts b/lib/seed/services/geolocation/models/BaseGeoSimpleModel.ts new file mode 100644 index 0000000..dffb17e --- /dev/null +++ b/lib/seed/services/geolocation/models/BaseGeoSimpleModel.ts @@ -0,0 +1,83 @@ +import { ObjectType, Field, ArgsType, InputType, Args } from 'type-graphql'; + +import { GeolocAddressSearchComponent, GeolocDistComponent, GeolocSearchComponent, PlaceComponent, PlaceComponentOptionnalWithAddress } from '@seed/interfaces/components.geo'; +import { GetManyArgs } from '@seed/graphql/Request'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import BaseSEOSimpleModel, { EditBaseSEOSimpleInput, NewBaseSEOSimpleInput } from '@seed/graphql/baseModels/BaseSeoSimpleModel'; +import { getDistanceBetween } from '../services/GeolocService'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class BaseGeoSimpleSEOModel extends BaseSEOSimpleModel { + public constructor(collectionName?: string, perm?: Permission) { + const cName = collectionName ? collectionName : 'articles'; + const permission = perm ? perm : permissions; + super(cName, permission); + } + + @Field(() => PlaceComponent) + place: PlaceComponent; + + @Field(() => GeolocDistComponent, { nullable: true }) + dist?: GeolocDistComponent; + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + @Field(() => Number, { nullable: true }) + async getDistFromLocation(@Args() location: GeolocSearchComponent) { + return getDistanceBetween( + { + latitude: this.place.loc.coordinates[1], + longitude: this.place.loc.coordinates[0], + }, + location, + ); + } + + searchOptions(): string[] { + return []; + } + filterOptions(): string[] { + return []; + } +} + +@ArgsType() +export class BaseGeoArgs extends GetManyArgs { + @Field(() => GeolocSearchComponent, { nullable: true }) + geoSearch?: GeolocSearchComponent; + + @Field(() => GeolocAddressSearchComponent, { nullable: true }) + geoSearchWithAddress?: GeolocAddressSearchComponent; +} + +@InputType() +export class NewGeoBaseSEOSimpleInput extends NewBaseSEOSimpleInput implements Partial { + @Field(() => PlaceComponent) + place: PlaceComponent; +} + +@InputType() +export class EditGeoBaseSEOSimpleInput extends EditBaseSEOSimpleInput implements Partial { + @Field(() => PlaceComponent, { nullable: true }) + place?: PlaceComponent; +} + +@InputType() +export class NewGeoWithAddressBaseSEOSimpleInput extends NewBaseSEOSimpleInput implements Partial { + @Field(() => PlaceComponentOptionnalWithAddress) + place: PlaceComponentOptionnalWithAddress; +} + +@InputType() +export class EditGeoWithAddressBaseSEOSimpleInput extends EditBaseSEOSimpleInput implements Partial { + @Field(() => PlaceComponentOptionnalWithAddress, { nullable: true }) + place?: PlaceComponentOptionnalWithAddress; +} diff --git a/lib/seed/services/geolocation/services/GeolocService.ts b/lib/seed/services/geolocation/services/GeolocService.ts new file mode 100644 index 0000000..6a3b32d --- /dev/null +++ b/lib/seed/services/geolocation/services/GeolocService.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import env from '@config/.env.json'; + +import { getDistance } from 'geolib'; + +export const getDistanceBetween = (from: { latitude: number; longitude: number }, to: { latitude: number; longitude: number }): any => { + const res = getDistance(from, to); + return res.toFixed(2); +}; diff --git a/lib/seed/services/geolocation/services/GooglePlaceService.ts b/lib/seed/services/geolocation/services/GooglePlaceService.ts new file mode 100644 index 0000000..07302e5 --- /dev/null +++ b/lib/seed/services/geolocation/services/GooglePlaceService.ts @@ -0,0 +1,173 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { Client, PlaceAutocompleteResult } from '@googlemaps/google-maps-services-js'; +import { AvailableTranslation } from '@src/__components/components'; + +import { CountryCodesComponentEnum } from '@seed/services/geolocation/components/countries'; +import _ from 'lodash'; +import { AddressStrictComponent, PlaceComponent } from '@seed/interfaces/components.geo'; + +const client = new Client({}); + +export const getGeoLoc = async (address: { + number?: string; + street?: string; + zip?: string; + city?: string; + country?: string; +}): Promise<{ loc: any; formattedAddress: string }> => { + let addString; + addString = address.street || ''; + addString += ' ' + (address.number || ''); + addString += ' ' + (address.zip || ''); + addString += ' ' + (address.city || ''); + addString += ' ' + (address.country || ''); + + try { + const res = await client.geocode({ + params: { + address: addString, + key: process.env.GOOGLE_MAP_apiKey || '', + }, + timeout: 1000, // milliseconds + }); + + if (res.status != 200) throw 'no_result'; + + const loc = { + type: 'Point', + coordinates: [res.data.results[0].geometry.location.lng, res.data.results[0].geometry.location.lng], + }; + + return { + loc: loc, + formattedAddress: res[0].formattedAddress, + }; + } catch (err) { + throw err; + } +}; + +export const getGeoLocFromAddress = async (address: string): Promise<{ loc: any; formattedAddress: string }> => { + try { + const res = await client.geocode({ + params: { + address: address, + key: process.env.GOOGLE_MAP_apiKey || '', + }, + timeout: 1000, // milliseconds + }); + + if (res.status != 200) throw 'no_result'; + + const loc = { + type: 'Point', + coordinates: [res.data.results[0].geometry.location.lng, res.data.results[0].geometry.location.lat], + }; + + return { + loc: loc, + formattedAddress: res.data.results[0].formatted_address, + }; + } catch (err) { + throw err; + } +}; + +export const getPlaceFromAddress = async (address: AddressStrictComponent): Promise => { + try { + const res = await client.geocode({ + params: { + address: address.returnFullAddress(), + key: process.env.GOOGLE_MAP_apiKey || '', + }, + timeout: 1000, // milliseconds + }); + + if (res.status != 200) throw 'no_result'; + + const loc = { + type: 'Point', + coordinates: [res.data.results[0].geometry.location.lng, res.data.results[0].geometry.location.lat], + }; + + return { + placeId: res.data.results[0].place_id, + address, + loc: loc, + formattedAddress: res.data.results[0].formatted_address, + }; + } catch (err) { + throw err; + } +}; + + +export const getPlaceDetail = async ( + session: string, + placeId: string, +): Promise<{ loc: any; lat: number; lng: number; formattedAddress: string; otherInfo: any }> => { + try { + const res = await client.placeDetails({ + params: { + // eslint-disable-next-line @typescript-eslint/camelcase + place_id: placeId, + sessiontoken: session, + fields: ['address_component', 'adr_address', 'business_status', 'formatted_address', 'geometry', 'icon', 'name', 'photo'], + key: process.env.GOOGLE_MAP_apiKey || '', + }, + timeout: 1000, // milliseconds + }); + + if (res.status != 200) throw 'no_result'; + + if (res.data.result.geometry) { + const loc = { + type: 'Point', + coordinates: [res.data.result.geometry.location.lng, res.data.result.geometry.location.lat], + }; + + return { + loc: loc, + lat: res.data.result.geometry.location.lat, + lng: res.data.result.geometry.location.lng, + formattedAddress: res.data.result.formatted_address as string, + otherInfo: res.data.result, + }; + } else throw 'no_result'; + } catch (err) { + throw err; + } +}; + +export const autocompleteService = async ( + session: string, + input: string, + language: AvailableTranslation = AvailableTranslation.en, + countries?: CountryCodesComponentEnum[], +): Promise => { + const params: any = { + key: process.env.GOOGLE_MAP_apiKey || '', + input, + language, + sessiontoken: session, + }; + + if (countries) { + params.components = _.map(countries, (country) => { + return 'country:' + country.toLowerCase(); + }); + } + + try { + const res = await client.placeAutocomplete({ + params: params, + timeout: 1000, // milliseconds + }); + + if (res.status != 200) throw 'no_result'; + + return res.data.predictions; + } catch (err) { + throw err; + } +}; diff --git a/lib/seed/services/hooks/hooks.decorator.ts b/lib/seed/services/hooks/hooks.decorator.ts new file mode 100644 index 0000000..73ad712 --- /dev/null +++ b/lib/seed/services/hooks/hooks.decorator.ts @@ -0,0 +1,126 @@ +/* eslint-disable prefer-rest-params */ +import { PostHookStatus, StreamOperationType } from '@seed/engine/utils/streams/schemas/stream.components'; +import { createAsyncStream } from '@seed/engine/utils/streams/stream.service'; +import AWS from 'aws-sdk'; +import { v4 as uuid } from 'uuid'; +import DB from '../database/DBService'; + +export function asyncHookDecorator() { + return function (target: TFunction): any { + for (const prop of Object.getOwnPropertyNames(target.prototype)) { + // console.log(prop); + const oldFunc: Function = target.prototype[prop]; + + const triggerName = prop; + + if (oldFunc instanceof Function) { + target.prototype[prop] = async function (): Promise { + if (process.env.NODE_ENV === 'local' || process.env.isHook == 'true') { + console.log('[NOW - HOOKS]', triggerName); + return oldFunc.apply(this, arguments); + } else { + const functionName = process.env.AWS_Hooks || process.env.AWS_compute || ''; + + if (functionName && functionName != '') { + const params = { + FunctionName: functionName, + InvocationType: 'Event', + Payload: JSON.stringify({ trigger: triggerName }), + }; + + await createAsyncStream(triggerName as never, arguments); + + AWS.config.region = process.env.awsRegion || 'eu-west-1'; + const lambda = new AWS.Lambda(); + + try { + await lambda.invoke(params).promise(); + console.log('[ASYNC - HOOKS] - INVOKING SENT', triggerName); + } catch (error) { + console.error('[ASYNC - HOOKS] - ERROR', error); + } + + return; + } + + return oldFunc.apply(this, arguments); + } + }; + } + } + }; +} + +export async function asyncHook(data: any): Promise { + if (process.env.NODE_ENV === 'local') { + console.log('[LOCAL] - HOOKS', data.operation); + return true; + } else { + const functionName = process.env.AWS_Hooks || process.env.AWS_compute || ''; + + if (functionName && functionName != '') { + const params = { + FunctionName: functionName, + InvocationType: 'Event', + Payload: JSON.stringify({ streamId: data._id }), + }; + + AWS.config.region = process.env.awsRegion || 'eu-west-1'; + const lambda = new AWS.Lambda(); + + try { + await lambda.invoke(params).promise(); + console.log('[ASYNC - HOOKS] - INVOKING SENT', data.operation, data.collection); + } catch (error) { + console.error('[ASYNC - HOOKS] - ERROR', error); + } + } + } + return false; +} + +export async function asyncHookAndStream(hookName: string, data: any): Promise { + if (process.env.NODE_ENV === 'local') { + console.log('[LOCAL] - HOOKS', hookName); + return true; + } else if (process.env.isHook == 'true') { + console.log('[ONLINE] - HOOKS', hookName); + return true; + } else { + const functionName = process.env.AWS_Hooks || process.env.AWS_compute || ''; + + if (functionName && functionName != '') { + const changeStreamId = uuid(); + const changeStreamData = { + _id: changeStreamId, + operation: StreamOperationType.hook, + collection: hookName, + documentKey: '', + insertedValues: data, + hookStatus: PostHookStatus.new, + createdAt: new Date(), + updateAt: new Date(), + }; + await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData); + + const params = { + FunctionName: functionName, + InvocationType: 'Event', + Payload: JSON.stringify({ streamId: changeStreamId }), + }; + + AWS.config.region = process.env.awsRegion || 'eu-west-1'; + const lambda = new AWS.Lambda(); + + try { + await lambda.invoke(params).promise(); + console.log('[ASYNC - HOOKS] - INVOKING SENT', hookName, changeStreamData._id); + } catch (error) { + console.error('[ASYNC - HOOKS] - ERROR', error); + } + } else { + console.error('[ASYNC - HOOKS] - NO COMPUTE'); + } + } + return false; +} diff --git a/lib/seed/services/notifications/helpers/notification.helper.ts b/lib/seed/services/notifications/helpers/notification.helper.ts new file mode 100644 index 0000000..38bc458 --- /dev/null +++ b/lib/seed/services/notifications/helpers/notification.helper.ts @@ -0,0 +1,59 @@ +import { EngineModel } from '@seed/engine/EngineModel'; +import { AvailableTranslation } from '@src/__components/components'; +import _ from 'lodash'; +import { NotificationInput } from '../services/NotificationService'; +import { NotificationType, ReceiverData } from '../schemas/notifications.schema'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; + +export const getNotificationsTypesFromInput = (input: NotificationInput): NotificationType[] => { + const types: NotificationType[] = []; + + if (input.emails) types.push(NotificationType.mail); + if (input.pushTokens) types.push(NotificationType.mail); + if (input.sms) types.push(NotificationType.sms); + + return types; +}; + +export const getReceiverDataFromLanguage = (input: NotificationInput): typeof receiverData => { + const receiverData: { + emails?: ReceiverData[]; + pushTokens?: ReceiverData[]; + sms?: ReceiverData[]; + } = {}; + + const finalLanguage = input.language || (process.env.DEFAULT_LANGUAGE as any) || AvailableTranslation.en; + + if (input.emails) { + if(!_.isArray(input.emails)) input.emails = [input.emails]; + + receiverData.emails = _.map(input.emails, (email) => { + return { + value: email, + language: finalLanguage, + }; + }); + } + if (input.pushTokens) { + if(!_.isArray(input.pushTokens)) input.pushTokens = [input.pushTokens]; + + receiverData.pushTokens = _.map(input.pushTokens, (pushTokens) => { + return { + value: pushTokens, + language: finalLanguage, + }; + }); + } + if (input.sms) { + if(!_.isArray(input.sms)) input.sms = [input.sms]; + + receiverData.sms = _.map(input.sms, (sms) => { + return { + value: sms, + language: finalLanguage, + }; + }); + } + + return receiverData; +}; diff --git a/lib/seed/services/notifications/helpers/slack.helper.ts b/lib/seed/services/notifications/helpers/slack.helper.ts new file mode 100644 index 0000000..bc6824e --- /dev/null +++ b/lib/seed/services/notifications/helpers/slack.helper.ts @@ -0,0 +1,60 @@ +import { fetchCall } from '@seed/helpers/FetchHelper'; +import { interpolate, promiseAll } from '@seed/helpers/Utils'; +import EmailSettingsModel from '@services/module-cms/functions/emails-settings/emails-settings.model'; +import { AvailableTranslation } from '@src/__components/components'; + +export const sendToSlack = async (data, webhookUrl) => { + return fetchCall({ + url: webhookUrl, + method: 'POST', + body: data, + raw: true, + headers: { 'Content-Type': 'application/json' }, + }); +}; + +export const sendSlackHelper = async (emailSettings: EmailSettingsModel, data: any): Promise => { + const slackSettings = emailSettings.slackSettings; + if (!slackSettings) { + console.error('No slack settings found'); + return false; + } + const webhooksUrls = slackSettings.webhookUrls; + const text = interpolate(slackSettings.text, data, { withEol: true }); + + const slackNotification: any = { + username: slackSettings.username || 'Project User', + text, + // eslint-disable-next-line @typescript-eslint/camelcase + icon_emoji: slackSettings.icon || ':rocket:', // User icon, you can also use custom icons here + }; + + if (slackSettings.actions) { + const actions: any[] = []; + slackSettings.actions.forEach((element) => { + const a: any = { + type: 'button', + text: element.text, + url: element.url, + }; + if (element.style) a.style = element.style; + actions.push(a); + }); + slackNotification.attachments = [ + { + actions, + }, + ]; + } + + const promises: Promise[] = []; + + webhooksUrls.forEach((element) => { + promises.push(sendToSlack(slackNotification, element)); + }); + + const promiseResults = await promiseAll(promises); + console.log(promiseResults); + + return true; +}; diff --git a/lib/seed/services/notifications/helpers/sms.helper.ts b/lib/seed/services/notifications/helpers/sms.helper.ts new file mode 100644 index 0000000..e58c92d --- /dev/null +++ b/lib/seed/services/notifications/helpers/sms.helper.ts @@ -0,0 +1,63 @@ +import { fetchCall } from '@seed/helpers/FetchHelper'; +import { interpolate, promiseAll } from '@seed/helpers/Utils'; +import EmailSettingsModel from '@services/module-cms/functions/emails-settings/emails-settings.model'; +import { AvailableTranslation } from '@src/__components/components'; + +export const sendSmsHelper = async ( + emailSettings: EmailSettingsModel, + finalLanguage: AvailableTranslation, + numbers: string | string[], + data: any, +): Promise => { + const smsSettings = emailSettings.smsSettings; + if (!smsSettings || !smsSettings.text[finalLanguage]) { + console.error('No sms settings found for this language', finalLanguage); + return false; + } + + //const toNumbers: any[] = []; + + if (!Array.isArray(numbers)) numbers = [numbers]; + /* for (let index = 0; index < numbers.length; index++) { + toNumbers.push({ Email: numbers[index] }); + } */ + + const body = interpolate(smsSettings.text[finalLanguage] || '', data); + + const promises: Promise[] = []; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const twilio = require('twilio'); + + const client = new twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); + + numbers.forEach((element) => { + const dataToSend = { + body: body, + from: process.env.TWILIO_FROM_NUMBER || '', + to: element, + }; + + console.log('SMS - Sending to', dataToSend); + + promises.push( + /* fetchCall({ + url: `https://api.twilio.com/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/Messages.json`, + method: 'POST', + query: dataToSend, + headers: { + Authorization: 'Basic ' + Buffer.from(`${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`).toString('base64'), + "Content-Type": 'x-www-form-urlencoded', + }, + }), */ + + client.messages.create({ + ...dataToSend, + }), + ); + }); + + const promiseResults = await promiseAll(promises); + console.log(promiseResults); + + return true; +}; diff --git a/lib/seed/services/notifications/notifications.model.ts b/lib/seed/services/notifications/notifications.model.ts new file mode 100644 index 0000000..e939328 --- /dev/null +++ b/lib/seed/services/notifications/notifications.model.ts @@ -0,0 +1,41 @@ +import { ObjectType, Field, ID, ArgsType, InputType, registerEnumType, Int } from 'type-graphql'; + +import { Permission } from '@seed/interfaces/permission'; + +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { EngineModel } from '@seed/engine/EngineModel'; +import { NotificationDBInterfaceSchema, NotificationDBSchema, NotificationSchema } from './schemas/notifications.schema'; +import { plainToClass } from 'class-transformer'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import { sendNotificationHanlder } from './services/NotificationService'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class NotificationModel extends EngineModel { + public constructor(input?: NotificationDBInterfaceSchema & Partial) { + const dataInit = plainToClass(NotificationDBSchema, input || {}); + super({ + collectionName: 'notifications', + permissions: permissions, + dataInit, + }); + } + + plainToClass(plain: any): NotificationSchema | NotificationSchema[] { + return plainToClass(NotificationSchema, plain); + } + + async afterCreate(): Promise { + await sendNotificationHanlder(this); + } + + async afterUpdate(): Promise {} + + async afterDelete(): Promise {} +} diff --git a/lib/seed/services/notifications/schemas/notifications.schema.ts b/lib/seed/services/notifications/schemas/notifications.schema.ts new file mode 100644 index 0000000..ccb92cf --- /dev/null +++ b/lib/seed/services/notifications/schemas/notifications.schema.ts @@ -0,0 +1,82 @@ +import { NotificationEnum } from '@config/config'; +import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema'; +import { AvailableTranslation } from '@src/__components/components'; +import { GraphQLJSONObject } from 'graphql-type-json'; +import { InputType, ObjectType, Field, Int, registerEnumType } from 'type-graphql'; + +export enum NotificationType { + 'mail' = 'mail', + 'push' = 'push', + 'sms' = 'sms', +} + +registerEnumType(NotificationType, { + name: 'NotificationType', +}); + +export enum NotificationStatus { + 'toSend' = 'toSend', + 'sent' = 'sent', + 'error' = 'error', +} + +registerEnumType(NotificationStatus, { + name: 'NotificationStatus', +}); + +@ObjectType() +@InputType() +export class ReceiverData { + @Field() + value: string; + + @Field(() => AvailableTranslation) + language: AvailableTranslation; +} + +@ObjectType() +@InputType() +export class NotificationBaseSchema { + @Field() + ressourceId: string; + + @Field(() => [String]) + accountIds?: string[]; + + data: any; + + @Field(() => NotificationEnum) + key: NotificationEnum; + + @Field(() => [ReceiverData], { nullable: true }) + emails?: ReceiverData[]; + @Field(() => [ReceiverData], { nullable: true }) + pushTokens?: ReceiverData[]; + @Field(() => [ReceiverData], { nullable: true }) + sms?: ReceiverData[]; + + @Field({ nullable: true }) + slack?: boolean; + + @Field(() => NotificationType) + status: NotificationStatus; + + @Field({ nullable: true }) + sentAt?: Date; +} + +@ObjectType() +export class NotificationDBInterfaceSchema extends NotificationBaseSchema {} + +@ObjectType() +export class NotificationDBSchema extends NotificationDBInterfaceSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class NotificationSchema extends NotificationDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/lib/seed/services/notifications/services/NotificationService.ts b/lib/seed/services/notifications/services/NotificationService.ts new file mode 100644 index 0000000..8b878be --- /dev/null +++ b/lib/seed/services/notifications/services/NotificationService.ts @@ -0,0 +1,93 @@ +import { AvailableTranslation } from '@src/__components/components'; +import { promiseAll } from '@seed/helpers/Utils'; +import EmailSettingsModel from '@services/module-cms/functions/emails-settings/emails-settings.model'; +import { sendEmailsHelper } from '../../email/EmailHelper'; +import NotificationModel from '../notifications.model'; +import _ from 'lodash'; +import { NotificationDBInterfaceSchema, NotificationStatus } from '../schemas/notifications.schema'; +import { getReceiverDataFromLanguage } from '../helpers/notification.helper'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import AccountModel from '@src/accounts/account.model'; +import { sendSmsHelper } from '../helpers/sms.helper'; +import { sendSlackHelper } from '../helpers/slack.helper'; +import { NotificationEnum } from '@config/config'; +import { EngineNotificationEnum } from '@seed/interfaces/components.notifications'; +import { SettingsCache } from '@seed/graphql/Settings'; + +export interface NotificationInput { + ressource: T | any; + key: NotificationEnum & EngineNotificationEnum | NotificationEnum; + emails?: string[] | string; + pushTokens?: string[] | string; + sms?: string[] | string; + slack?: boolean; + language?: AvailableTranslation; + account?: AccountModel; +} + +export const sendNotification = async (input: NotificationInput): Promise => { + const { ressource, key, account } = input; + + // Create the notification + const notificationData: NotificationDBInterfaceSchema = { + ressourceId: ressource._id || 'no-data', + data: ressource, + key, + ...getReceiverDataFromLanguage(input), + slack: input.slack, + status: NotificationStatus.toSend, + }; + + if (account) notificationData.accountIds = [account._id]; + + const notificationModel = new NotificationModel(notificationData); + + await notificationModel.saveOne({}); + + return true; +}; + +export const sendNotificationHanlder = async (input: NotificationModel): Promise => { + const { data, key, emails, sms, slack, accountIds } = input.get(); + + let emailSettings: EmailSettingsModel[]; + try { + emailSettings = SettingsCache.getInstance().cache.emailsContent[key]; + } catch (error) { + console.error('NOTIFICATION.ERROR', { message: 'notfound', key }); + return false; + } + + const promises: any[] = []; + + emailSettings.forEach(emailSetting => { + // Email flows + if (emails) { + // Send one per language + const emailsByLanguage = _.groupBy(emails, 'language'); + for (const language in emailsByLanguage) { + const emails = _.map(emailsByLanguage[language], 'value'); + promises.push(sendEmailsHelper(emailSetting, language as any, emails, data)); + } + } + if (sms) { + // Send one per language + const smsByLanguage = _.groupBy(sms, 'language'); + for (const language in smsByLanguage) { + const numbers = _.map(smsByLanguage[language], 'value'); + promises.push(sendSmsHelper(emailSetting, language as any, numbers, data)); + } + } + if (slack) { + promises.push(sendSlackHelper(emailSetting, data)); + } + }) + + + const promiseResults = await promiseAll(promises); + console.log(promiseResults); + + await input.updateOne({ query: { _id: input._id }, newData: { status: NotificationStatus.sent } }); + + return true; +}; diff --git a/lib/seed/webpack.config.js b/lib/seed/webpack.config.js new file mode 100644 index 0000000..1ffc9f1 --- /dev/null +++ b/lib/seed/webpack.config.js @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require('path'); +const nodeExternals = require('webpack-node-externals'); +const slsw = require('serverless-webpack'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const TsConfigPathsPlugin = require('awesome-typescript-loader').TsConfigPathsPlugin; +const CopyPlugin = require("copy-webpack-plugin"); + + +const isLocal = slsw.lib.webpack.isLocal; +const stage = slsw.lib.options.stage; + +console.log('[Webpack - RUN]', new Date()); +console.log('[Webpack - RUN] Entry', slsw.lib.entries); +console.log('[Webpack - RUN] Stage', stage); + +module.exports = { + mode: isLocal ? 'development' : 'production', + entry: slsw.lib.entries, + externals: [nodeExternals({ + allowlist:["axios"], + })], + devtool: 'source-map', + resolve: { + extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], + plugins: [new TsConfigPathsPlugin()], + }, + output: { + libraryTarget: 'commonjs2', + path: path.join(__dirname, '.webpack'), + filename: '[name].js', + }, + target: 'node', + module: { + rules: [ + // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` + { test: /\.tsx?$/, loader: 'awesome-typescript-loader', options: { transpileOnly: true }, exclude: /node_modules/ }, + ], + }, + plugins: [new ForkTsCheckerWebpackPlugin(), + new CopyPlugin({ + patterns: [ + { from: "node_modules/axios", to: "node_modules/axios" }, + ], + }),], + optimization: { + minimize: false, // <---- disables uglify. + nodeEnv: stage, + }, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..a04bb54 --- /dev/null +++ b/package.json @@ -0,0 +1,121 @@ +{ + "name": "workinflex", + "version": "1.0.0", + "description": "Work in Flex by Make it", + "devops": { + "awsProfile": "workinflex", + "awsRegion": "eu-central-1", + "services": { + "admin": [ + "./lib/seed/devops", + "serverless-admin.yaml", + "handler-admin.ts" + ], + "app": [ + "./lib/seed/devops", + "serverless-app.yaml", + "handler-app.ts" + ], + "hooks": [ + "./lib/seed/devops", + "serverless-hooks.yaml", + "handler-hooks.ts" + ], + "uploads": [ + "./services/api-uploads", + "serverless-upload.yaml" + ], + "messaging": [ + "./services/api-messaging", + "serverless-messaging.yaml" + ], + "stripe-hooks": [ + "./services/module-payments", + "serverless-payments.yaml", + "__handlers/stripeHookHandler.ts" + ], + "cronjobs": [ + "./lib/__cronjobs", + "serverless-cron.yaml", + "handler.ts" + ] + } + }, + "scripts": { + "dev:app": "ts-node-dev --respawn --inspect=9922 -r tsconfig-paths/register ./lib/seed/app.ts", + "dev:admin": "ts-node-dev --respawn --inspect=9922 -r tsconfig-paths/register ./lib/seed/app-admin.ts", + "dev:messaging": "ts-node-dev --respawn --inspect=9922 -r tsconfig-paths/register ./services/api-messaging/app.ts", + "deploy:dev": "node ./lib/seed/devops/deploy.js dev", + "deploy:staging": "node ./lib/seed/devops/deploy.js staging", + "deploy:production": "node ./lib/seed/devops/deploy.js production", + "deploy:domains": "node ./lib/seed/devops/deploy-domains.js", + "test:gen:app": "zeus http://localhost:4000 ./__tests/app/helpers --ts", + "test:gen:admin": "zeus http://localhost:4001 ./__tests/admin/helpers --ts", + "test:app": "ts-node-dev --respawn -r tsconfig-paths/register ./__tests/app", + "test:admin": "ts-node-dev --respawn -r tsconfig-paths/register ./__tests/admin", + "tests:makeit-example": "ts-node-dev --respawn --transpileOnly -r tsconfig-paths/register ./__tests/makeit-example" + }, + "author": "Sanawar Syed - Makeit", + "license": "MIT", + "devDependencies": { + "@types/graphql": "^14.5.0", + "@types/graphql-iso-date": "^3.4.0", + "@types/luxon": "^1.26.2", + "@types/mongodb": "^3.6.10", + "@types/node": "^14.14.35", + "@typescript-eslint/eslint-plugin": "^4.18.0", + "@typescript-eslint/parser": "^4.18.0", + "apollo-server": "^2.21.1", + "awesome-typescript-loader": "^5.2.1", + "cache-loader": "^4.1.0", + "copy-webpack-plugin": "^8.0.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.22.1", + "execa": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.1.1", + "graphql-zeus": "^3.0.5", + "listr": "^0.14.3", + "serverless": "^2.30.3", + "serverless-domain-manager": "^5.1.0", + "serverless-offline": "^6.8.0", + "serverless-webpack": "^5.4.0", + "ts-node": "^9.1.1", + "ts-node-dev": "^1.1.6", + "tsconfig-paths": "^3.9.0", + "typescript": "^4.2.3", + "webpack": "^5.26.3", + "webpack-node-externals": "^2.5.2" + }, + "dependencies": { + "@googlemaps/google-maps-services-js": "^3.1.16", + "apollo-server-lambda": "^2.21.1", + "axios": "^0.21.1", + "class-transformer": "^0.4.0", + "class-validator": "^0.13.1", + "dataloader": "^2.0.0", + "dotenv": "^8.2.0", + "file-type": "^16.3.0", + "firebase-admin": "^9.5.0", + "geolib": "^3.3.1", + "googleapis": "^67.1.1", + "graphql": "^15.5.0", + "graphql-iso-date": "^3.6.1", + "graphql-type-json": "^0.3.2", + "jimp": "^0.16.1", + "lodash": "^4.17.21", + "luxon": "^1.26.0", + "moment": "^2.29.1", + "moment-timezone": "^0.5.33", + "mongodb": "^3.6.5", + "node-fetch": "^2.6.1", + "node-geocoder": "^3.27.0", + "node-mailjet": "^3.3.1", + "reflect-metadata": "^0.1.13", + "retry-axios": "^2.4.0", + "serverless-http": "^2.7.0", + "stripe": "^8.138.0", + "twilio": "^3.58.0", + "type-graphql": "^1.1.1", + "uuid": "^8.3.2" + } +} diff --git a/serverless.yaml b/serverless.yaml new file mode 100644 index 0000000..1ce56d4 --- /dev/null +++ b/serverless.yaml @@ -0,0 +1,44 @@ +app: ${file(./package.json):name} +<<<<<<< HEAD +service: cronjob +======= +service: hooks +>>>>>>> develop +package: + individually: false +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + AWS_compute: ${self:service}-${self:provider.stage}-compute + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' +plugins: + - serverless-webpack +custom: + stage: ${opt:stage, self:provider.stage} + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk + - puppeteer + +functions: +<<<<<<< HEAD + removeDraftBookings: + handler: lib/__cronjobs/handler.removeDraftBookings + timeout: 300 # optional, in seconds, default is 6 + events: + - schedule: rate(1 minute) +======= + hookHandler: + handler: handler.hookHandler + memorySize: 1024 # optional, in MB, default is 1024 + timeout: 600 # optional, in seconds, default is 6 + async: true +>>>>>>> develop diff --git a/services/api-messaging/README.md b/services/api-messaging/README.md new file mode 100644 index 0000000..71b12f8 --- /dev/null +++ b/services/api-messaging/README.md @@ -0,0 +1,6 @@ +# api-messaging + +# Make-it messaging service +[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com) + +![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) diff --git a/services/api-messaging/__tests/playground.gql b/services/api-messaging/__tests/playground.gql new file mode 100644 index 0000000..325f355 --- /dev/null +++ b/services/api-messaging/__tests/playground.gql @@ -0,0 +1,67 @@ +mutation addRoom { + roomsCreateOne(input: { accountIds: ["kbRiSJEjHbX3KOEcF5IK41M4SgH2"] }) { + _id + title + accountIds + getAccounts { + _id + userName + } + } +} + +query myRooms { + roomsGetMany { + _id + title + accountIds + getAccounts { + _id + userName + } + } +} + +query myRoom { + roomsGetOne(id: "8e8099a2-5645-448e-981e-8a97c5beee5f") { + _id + title + accountIds + getAccounts { + _id + userName + } + } +} + +mutation addMessage { + roomsAddOneMessage(input: { roomId: "8e8099a2-5645-448e-981e-8a97c5beee5f", message: "Hello again" }) { + _id + message + sentBy + } +} + +query getOneRoomMessages { + roomsGetOneMessages(roomId: "8e8099a2-5645-448e-981e-8a97c5beee5f", pagination: { sort: "createdAt desc" }) { + _id + message + readBy { + accountId + readAt + } + createdAt + } +} + +mutation markMessageAsRead { + roomsMarkMessageAsRead(id: "ea279bcf-52c7-4ccd-b04f-1a586ebd15e5") { + _id + message + sentBy + readBy { + accountId + readAt + } + } +} diff --git a/services/api-messaging/app.ts b/services/api-messaging/app.ts new file mode 100644 index 0000000..1b52890 --- /dev/null +++ b/services/api-messaging/app.ts @@ -0,0 +1,74 @@ +import 'reflect-metadata'; +import { buildTypeDefsAndResolvers, buildSchema, NonEmptyArray } from 'type-graphql'; + +import env from '@config/.env.json'; + +import { authMiddleware, errorMiddleware, ctxMiddleware, complexityMiddleware } from '@seed/graphql/Middleware'; + +import Firebase from '@seed/services/auth/FirebaseService'; +import { ApiMessagingResolver } from './handler'; +import { SettingsCache } from '@seed/graphql/Settings'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { ApolloServer } = require('apollo-server'); + +const PORT = process.env.PORT || 4000; + +const bootstrap = async (): Promise => { + const settingsI = SettingsCache.getInstance(); + await settingsI.refreshCache(); + try { + // // eslint-disable-next-line @typescript-eslint/no-unused-vars + // let { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ + // resolvers: AppResolvers, + // authChecker: authMiddleware, + // }); + + // typeDefs = typeDefs.replace('scalar Upload', 'scalar UploadFix'); + // resolvers = resolvers; + + const schema = await buildSchema({ + resolvers: (ApiMessagingResolver as unknown) as NonEmptyArray, + authChecker: authMiddleware, + }); + + const server = new ApolloServer({ + // typeDefs, + // resolvers, + schema, + formatError: errorMiddleware, + formatResponse: (response): any => { + return response; + }, + context: ctxMiddleware, + tracing: true, + plugins: [ + { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + requestDidStart: () => ({ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + didResolveOperation({ request, document }) { + return complexityMiddleware(schema, request.variables, document); + }, + }), + }, + ], + }); + + server.listen(PORT).then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); + }); + + try { + const token = await Firebase.getInstance().createTokenId(process.env.TEST_USER_EMAIL ? process.env.TEST_USER_EMAIL : ''); + console.log(`Token for ${process.env.TEST_USER_EMAIL}`, token.idToken); + } catch (error) { + console.log('no token available'); + } + } catch (error) { + console.log('[GRAPH ERROR]', error); + throw error; + } +}; + +bootstrap(); diff --git a/services/api-messaging/components.ts b/services/api-messaging/components.ts new file mode 100644 index 0000000..9867ced --- /dev/null +++ b/services/api-messaging/components.ts @@ -0,0 +1,35 @@ +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +import { ObjectType, Field } from 'type-graphql'; + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@ObjectType() +export class UnreadCount { + @Field() + accountId: string; + @Field() + count: number; +} + +@ObjectType() +export class RoomsStats { + @Field(() => [UnreadCount]) + unreadCounts: UnreadCount[]; +} diff --git a/services/api-messaging/functions/messages/message.model.ts b/services/api-messaging/functions/messages/message.model.ts new file mode 100644 index 0000000..f18c075 --- /dev/null +++ b/services/api-messaging/functions/messages/message.model.ts @@ -0,0 +1,91 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import ImageComponent from '@seed/interfaces/components'; +import { GetArgs } from '@seed/graphql/Request'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export class MessageReadBy { + @Field() + accountId: string; + + @Field() + readAt: Date; +} + +@ObjectType() +export default class MessageModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'messaging.messages', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field() + roomId: string; + + @Field({nullable: true}) + notified?: boolean; + + @Field() + message: string; + @Field() + edited: boolean; + @Field() + deleted: boolean; + + @Field(() => ImageComponent, { nullable: true }) + file: ImageComponent; + + @Field() + sentBy: string; + + @Field(() => [MessageReadBy]) + readBy: MessageReadBy[]; + + searchOptions(): string[] { + return []; + } + filterOptions(): string[] { + return []; + } +} + +@ArgsType() +export class MessageArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@InputType() +export class NewMessageInput implements Partial { + @Field() + roomId: string; + + @Field() + message: string; + + @Field(() => ImageComponent, { nullable: true }) + file?: ImageComponent; +} + +@InputType() +export class EditMessageInput implements Partial { + @Field() + message: string; +} diff --git a/services/api-messaging/functions/messages/message.resolver.ts b/services/api-messaging/functions/messages/message.resolver.ts new file mode 100644 index 0000000..01fce63 --- /dev/null +++ b/services/api-messaging/functions/messages/message.resolver.ts @@ -0,0 +1,161 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized } from 'type-graphql'; + +import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric } from '@seed/graphql/BaseService'; +import MessageModel, { MessageArgs, NewMessageInput, EditMessageInput } from '@services/api-messaging/functions/messages/message.model'; + +import RoomResolver from '@services/api-messaging/functions/rooms/room.resolver'; +import { ApolloContext } from '@seed/interfaces/context'; +import { ApolloError } from 'apollo-server-lambda'; +import _ from 'lodash'; + +@Resolver(MessageModel) +export default class MessageResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + @FieldResolver() + message(@Root() mess: MessageModel, @Ctx() ctx: ApolloContext): string { + if (mess.deleted) return '[DELETED]'; + return mess.message; + } + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => MessageModel) + @Authorized() + async roomsAddOneMessage(@Arg('input') input: NewMessageInput, @Ctx() ctx: ApolloContext): Promise { + try { + // Get the Room first + const roomResolver = new RoomResolver(); + const room = await roomResolver.roomsGetOne(input.roomId, ctx); + + const model = new MessageModel(); + const result = model.saveOne( + { + ...input, + edited: false, + sentBy: ctx.ctx.user._id, + readBy: [], + r: room.r, + w: [ctx.ctx.user._id], + d: [ctx.ctx.user._id], + createdAt: new Date(), + updatedAt: new Date(), + }, + ctx, + ); + + // Todo - Refactor + await room.updateUnreadCounts(model, 1); + + await room.updateOne( + { _id: room._id }, + { + lastMessage: new Date(), + }, + null, + ); + + return result; + } catch (error) { + throw error; + } + } + + @Mutation(() => MessageModel) + @Authorized() + async roomsEditOneMessage(@Arg('id') id: string, @Arg('input') input: EditMessageInput, @Ctx() ctx: ApolloContext): Promise { + const model = new MessageModel(); + const message = await model.getOne({ _id: id }, ctx); + + if (message.sentBy !== ctx.ctx.user._id) + throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: message.sentBy }); + + return model.updateOne( + { _id: id }, + { + message: input.message, + edited: true, + updatedAt: new Date(), + }, + ctx, + ); + } + + @Mutation(() => MessageModel) + @Authorized() + async roomsMarkMessageAsRead(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new MessageModel(); + const message = await model.getOne({ _id: id }, ctx); + + // The hypothesis here is that the message are accessible only by concerned people. + // No need to double check with the rooms accountsId + + const newReadBy = _.uniqBy(message.readBy.concat({ accountId: ctx.ctx.user._id, readAt: new Date() }), 'accountId'); + const result = await model.updateOne( + { _id: id }, + { + readBy: newReadBy, + }, + null, + ); + + // Update stats & notify + // Get the Room first + const roomResolver = new RoomResolver(); + const room = await roomResolver.roomsGetOne(result.roomId, ctx); + + await room.updateUnreadCounts(result, -1); + + return result; + } + + @Mutation(() => MessageModel) + @Authorized() + async roomsDeleteOneMessage(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new MessageModel(); + const message = await model.getOne({ _id: id }, ctx); + + if (message.sentBy !== ctx.ctx.user._id) + throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: message.sentBy }); + + const result = await model.updateOne( + { _id: id }, + { + deleted: true, + updatedAt: new Date(), + }, + ctx, + ); + + // Update stats & notify + // Get the Room first + const roomResolver = new RoomResolver(); + const room = await roomResolver.roomsGetOne(result.roomId, ctx); + + await room.updateUnreadCounts(result, -1); + + return result; + } +} diff --git a/services/api-messaging/functions/rooms/room.model.ts b/services/api-messaging/functions/rooms/room.model.ts new file mode 100644 index 0000000..b492dd1 --- /dev/null +++ b/services/api-messaging/functions/rooms/room.model.ts @@ -0,0 +1,115 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; +import AccountModel from '@src/accounts/account.model'; +import { GetArgs } from '@seed/graphql/Request'; +import { ApolloContext } from '@seed/interfaces/context'; +import { ApolloError } from 'apollo-server-lambda'; +import { Stats } from 'fs'; +import { RoomsStats } from '@services/api-messaging/components'; +import MessageModel from '../messages/message.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import _ from 'lodash'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin, AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class RoomModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'messaging.rooms', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field(() => String, { nullable: true }) + title?: string; + + @Field(() => RoomsStats) + stats: RoomsStats; + + @Field(() => [AccountModel], { nullable: true }) + getAccounts?: AccountModel[]; + + @Field(() => [MessageModel], { nullable: true }) + getMessages?: MessageModel[]; + + @Field({ nullable: true }) + lastMessage: Date; + + searchOptions(): string[] { + return ['title']; + } + filterOptions(): string[] { + return ['title']; + } + + async updateUnreadCounts(newMessage: MessageModel, count: 1 | -1 | 0): Promise { + const { sentBy } = newMessage; + for (let index = 0; index < this.stats.unreadCounts.length; index++) { + // Add a count to all the other one + if (this.stats.unreadCounts[index].accountId != sentBy) { + if (count == 0) this.stats.unreadCounts[index].count = 0; + else this.stats.unreadCounts[index].count = this.stats.unreadCounts[index].count + count; + } + if (this.stats.unreadCounts[index].count < 0) this.stats.unreadCounts[index].count = 0; + } + + await (await this.db()).findOneAndUpdate( + { _id: this.get()._id }, + { $set: { stats: this.stats } as any }, + { upsert: false, returnOriginal: false }, + ); + } +} + +@ArgsType() +export class RoomArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@InputType() +export class NewRoomInput { + @Field(() => [String]) + accountIds: string[]; +} + +@InputType() +export class AddPeopleToRoomInput { + @Field() + roomId: string; + @Field(() => [String]) + accountIds: string[]; +} + +@InputType() +export class RemovePeopleFromRoomInput { + @Field() + roomId: string; + @Field(() => [String]) + accountIds: string[]; +} + +export const onCreate = async (input: NewRoomInput, ctx: ApolloContext): Promise => { + try { + // To refactor + const users = await ctx.ctx.loaders.accountLoader.loadMany(input.accountIds); + + input.accountIds.forEach((element) => { + if (_.findIndex(users, { _id: element }) == -1) throw new ApolloError('users.notFound', '400', { _id: element }); + }); + } catch (error) { + throw error; + } +}; diff --git a/services/api-messaging/functions/rooms/room.resolver.ts b/services/api-messaging/functions/rooms/room.resolver.ts new file mode 100644 index 0000000..f25e6ed --- /dev/null +++ b/services/api-messaging/functions/rooms/room.resolver.ts @@ -0,0 +1,211 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized } from 'type-graphql'; + +import { addOneGeneric } from '@seed/graphql/BaseService'; +import RoomModel, { + RoomArgs, + RemovePeopleFromRoomInput, + AddPeopleToRoomInput, + NewRoomInput, +} from '@services/api-messaging/functions/rooms/room.model'; +import { ApolloContext } from '@seed/interfaces/context'; +import { ApolloError } from 'apollo-server-lambda'; +import AccountModel from '@src/accounts/account.model'; +import { onCreate } from '@services/api-messaging/functions/rooms/room.model'; +import _ from 'lodash'; +import MessageModel, { MessageArgs } from '../messages/message.model'; +import { oneToManyComplexity } from '@seed/graphql/Middleware'; +import { UnreadCount } from '@services/api-messaging/components'; + +@Resolver(RoomModel) +export default class RoomResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + @FieldResolver() + async getAccounts(@Root() room: RoomModel, @Ctx() ctx: ApolloContext): Promise<(AccountModel | Error)[]> { + return await ctx.ctx.loaders.accountLoader.loadMany(room.r); + } + @FieldResolver(() => [MessageModel], { complexity: oneToManyComplexity }) + async getMessages(@Args() args: MessageArgs, @Root() room: RoomModel, @Ctx() ctx: ApolloContext): Promise { + try { + if (!args.pagination) args.pagination = { limit: 10, skip: 0, sort: 'createdAt desc' }; + else if (args.pagination && !args.pagination.sort) args.pagination = { ...args.pagination, sort: 'createdAt desc' }; + + return new MessageModel().getMany({ roomId: room._id }, args.pagination, ctx); + } catch (error) { + throw error; + } + } + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + @Query(() => RoomModel) + @Authorized() + async roomsGetOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new RoomModel(); + const roomData = await model.getOne({ _id: id }, ctx); + if (!roomData.r.includes(ctx.ctx.user._id)) + throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: roomData.r }); + + return roomData; + } + + @Query(() => [RoomModel]) + @Authorized() + async roomsGetMany(@Args() args: RoomArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new RoomModel(); + const result = await model.getMany({ accountIds: ctx.ctx.user._id }, args.pagination, ctx); + return result; + } + + @Query(() => [MessageModel]) + @Authorized() + async roomsGetMessages(@Arg('id') id: string, @Args() args: RoomArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new RoomModel(); + const roomData = await model.getOne({ _id: id }, ctx); + if (!roomData.r.includes(ctx.ctx.user._id)) + throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: roomData.r }); + + if (!args.pagination) args.pagination = { limit: 10, skip: 0, sort: 'createdAt desc' }; + else if (args.pagination && !args.pagination.sort) args.pagination = { ...args.pagination, sort: 'createdAt desc' }; + + return new MessageModel().getMany({ roomId: id }, args.pagination, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => RoomModel) + @Authorized() + async roomsCreateOne(@Arg('input') input: NewRoomInput, @Ctx() ctx: ApolloContext): Promise { + const model = new RoomModel(); + + try { + await onCreate(input, ctx); + } catch (error) { + throw error; + } + + input.accountIds.push(ctx.ctx.user._id); + input.accountIds = _.uniq(input.accountIds); + + // Check if a room with the same user doesn't exists already + + try { + return await model.getOne( + { + r: { $all: input.accountIds }, + }, + ctx, + ); + } catch (error) { + const unreadCounts: UnreadCount[] = []; + for (let index = 0; index < input.accountIds.length; index++) { + unreadCounts.push({ + accountId: input.accountIds[index], + count: 0, + }); + } + + const dataToSave: Partial = { + ...input, + r: input.accountIds, + w: input.accountIds, + stats: { + unreadCounts: unreadCounts, + }, + }; + return addOneGeneric(model, dataToSave, ctx); + } + } + + @Mutation(() => RoomModel) + @Authorized() + async roomsAddPeople(@Arg('input') input: AddPeopleToRoomInput, @Ctx() ctx: ApolloContext): Promise { + const model = new RoomModel(); + + // Get the room first to see if allowed + const roomData = await model.getOne({ _id: input.roomId }, ctx); + if (!roomData.r.includes(ctx.ctx.user._id)) + throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: roomData.r }); + + const newAccountIds = _.uniq(roomData.r.concat(input.accountIds)); + + try { + await onCreate(input, ctx); + } catch (error) { + throw error; + } + + // TODO : Remove from stats + return roomData.updateOne({ _id: input.roomId }, { r: newAccountIds, w: newAccountIds }, ctx); + } + + @Mutation(() => RoomModel) + @Authorized() + async roomsRemovePeople(@Arg('input') input: RemovePeopleFromRoomInput, @Ctx() ctx: ApolloContext): Promise { + const model = new RoomModel(); + + // Get the room first to see if allowed + const roomData = await model.getOne({ _id: input.roomId }, ctx); + if (!roomData.r.includes(ctx.ctx.user._id)) + throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: roomData.r }); + + const newAccountIds = _.uniq( + roomData.r.filter(function(element) { + return roomData.r.indexOf(element) === -1; + }), + ); + + // TODO : Remove from stats + + return roomData.updateOne({ _id: input.roomId }, { r: newAccountIds, w: newAccountIds }, ctx); + } + + @Mutation(() => RoomModel) + @Authorized() + async roomsMarkAllMessageAsRead(@Arg('roomId') roomId: string, @Ctx() ctx: ApolloContext): Promise { + const roomModel = new RoomModel(); + await roomModel.getOne({ _id: roomId }, ctx); + + // The hypothesis here is that the message are accessible only by concerned people. + const model = new MessageModel(); + await (await model.db()).updateMany( + { roomId: roomId, 'readBy.accountId': { $ne: ctx.ctx.user._id } }, + { + $push: { readBy: { accountId: ctx.ctx.user._id, readAt: new Date() } }, + }, + ); + + // Update stats & notify + for (let index = 0; index < roomModel.stats.unreadCounts.length; index++) { + // Add a count to all the other one + if (roomModel.stats.unreadCounts[index].accountId == ctx.ctx.user._id) roomModel.stats.unreadCounts[index].count = 0; + } + + await (await roomModel.db()).findOneAndUpdate( + { _id: roomModel._id }, + { $set: { stats: roomModel.stats } }, + { upsert: false, returnOriginal: false }, + ); + + return roomModel; + } +} diff --git a/services/api-messaging/functions/rooms/room.service.ts b/services/api-messaging/functions/rooms/room.service.ts new file mode 100644 index 0000000..5d18430 --- /dev/null +++ b/services/api-messaging/functions/rooms/room.service.ts @@ -0,0 +1,17 @@ +import { ApolloContext } from '@seed/interfaces/context'; +import RoomModel from './room.model'; +import RoomResolver from './room.resolver'; + +export const roomsGetOneByAccountsIds = async (ids: string[], toCreate?: { ctx: ApolloContext }): Promise => { + const model = new RoomModel(); + + try { + const roomData = await model.getOne({ r: { $all: ids } }, null); + + return roomData; + } catch (error) { + if (!toCreate) throw error; + + return await new RoomResolver().roomsCreateOne({ accountIds: ids }, toCreate.ctx); + } +}; diff --git a/services/api-messaging/serverless-messaging.yaml b/services/api-messaging/serverless-messaging.yaml new file mode 100644 index 0000000..f057034 --- /dev/null +++ b/services/api-messaging/serverless-messaging.yaml @@ -0,0 +1,40 @@ +app: ${file(./package.json):name} +service: messaging +package: + individually: true +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs10.x + environment: + AWS_compute: ${self:service}-${self:provider.stage}-compute + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' +plugins: + - serverless-webpack + - serverless-domain-manager +custom: + stage: ${opt:stage, self:provider.stage} + domains: ${file(./config/domains.yaml):domains} + customDomain: ${file(./config/domains.yaml):customDomain} + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk +functions: + handler: + handler: handler.handler + timeout: 30 + events: + - http: + path: / + method: post + cors: true + - http: + path: / + method: get + cors: true diff --git a/services/api-uploads/README.md b/services/api-uploads/README.md new file mode 100644 index 0000000..8e23cc8 --- /dev/null +++ b/services/api-uploads/README.md @@ -0,0 +1,9 @@ +# Make-it Upload Service + +[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com) + +![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) + +### Documentation + +https://docs.google.com/document/d/1Nw-9-QFOXBXZxrxHPCv6vojtwedWN2hVsihQr-NIza4/edit# diff --git a/services/api-uploads/__tests/playground.gql b/services/api-uploads/__tests/playground.gql new file mode 100644 index 0000000..c31eaef --- /dev/null +++ b/services/api-uploads/__tests/playground.gql @@ -0,0 +1,25 @@ +# Write your query or mutation here +mutation uploadFile { + uploadFile( + input: { + title: "myFile" + base64: "data:image/png;base64,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" + } + ) { + _id + large + medium + small + title + } +} + +query myFiles { + files { + _id + large + medium + small + title + } +} diff --git a/services/api-uploads/app.ts b/services/api-uploads/app.ts new file mode 100644 index 0000000..47e27d9 --- /dev/null +++ b/services/api-uploads/app.ts @@ -0,0 +1,79 @@ +import 'reflect-metadata'; +import { buildTypeDefsAndResolvers, buildSchema, NonEmptyArray } from 'type-graphql'; + +import { authMiddleware, errorMiddleware, ctxMiddleware, complexityMiddleware } from '@seed/graphql/Middleware'; + +import Firebase from '@seed/services/auth/FirebaseService'; +import { ApiUploadResolver } from './handler'; +import { SettingsCache } from '@seed/graphql/Settings'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { ApolloServer } = require('apollo-server'); + +const PORT = process.env.PORT || 4000; + +const bootstrap = async (): Promise => { + const settingsI = SettingsCache.getInstance(); + await settingsI.refreshCache(); + + try { + // // eslint-disable-next-line @typescript-eslint/no-unused-vars + // let { typeDefs, resolvers } = await buildTypeDefsAndResolvers({ + // resolvers: AppResolvers, + // authChecker: authMiddleware, + // }); + + // typeDefs = typeDefs.replace('scalar Upload', 'scalar UploadFix'); + // resolvers = resolvers; + + const schema = await buildSchema({ + resolvers: ([...ApiUploadResolver] as unknown) as NonEmptyArray, + authChecker: authMiddleware, + }); + + const server = new ApolloServer({ + // typeDefs, + // resolvers, + schema, + formatError: errorMiddleware, + formatResponse: (response): any => { + return response; + }, + context: ctxMiddleware, + tracing: true, + plugins: [ + { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + requestDidStart: () => ({ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + didResolveOperation({ request, document }) { + return complexityMiddleware(schema, request.variables, document); + }, + }), + }, + ], + }); + + const token = await Firebase.getInstance().createTokenId(process.env.TEST_ADMIN_EMAIL ? process.env.TEST_ADMIN_EMAIL : ''); + console.log(token); + + server.listen(PORT).then(({ url }) => { + console.log(`🚀 Server ready at ${url}`); + }); + + // // await listCustomFields(); + // const addres = await addContact({ + // email: 'sanawar@makeit-studio.com', + // firstName: 'Sanawar', + // lastName: 'Syed', + // phone: '+32484740444', + // }); + + // await editContactFields(addres, { stage: 'Phase 3' }); + } catch (error) { + console.log('[GRAPH ERROR]', error); + throw error; + } +}; + +bootstrap(); diff --git a/services/api-uploads/file.model.ts b/services/api-uploads/file.model.ts new file mode 100644 index 0000000..971f026 --- /dev/null +++ b/services/api-uploads/file.model.ts @@ -0,0 +1,92 @@ +import { ObjectType, Field, ID, ArgsType, InputType, registerEnumType } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetArgs } from '@seed/graphql/Request'; + +import AccountModel from '@src/accounts/account.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +export enum FileType { + image = 'image', + document = 'document', +} +registerEnumType(FileType, { + name: 'FileType', +}); + +@ObjectType() +export default class FileModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'files', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field() + title: string; + + @Field() + accountId: string; + + @Field(() => AccountModel, { nullable: true }) + account?: AccountModel; + + @Field() + fileType: FileType; + + @Field() + fileName: string; + + @Field() + large: string; + + @Field({ nullable: true }) + medium?: string; + + @Field({ nullable: true }) + small?: string; + + @Field({ nullable: true }) + fileNameMedium?: string; + + @Field({ nullable: true }) + fileNameSmall?: string; + + searchOptions(): string[] { + return ['title']; + } + filterOptions(): string[] { + return ['title']; + } +} + +@ArgsType() +export class FileArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} +@InputType() +export class NewFileInput { + @Field() + title: string; + + @Field() + base64: string; + + @Field({ nullable: true }) + privateFile?: boolean; +} diff --git a/services/api-uploads/file.resolver.ts b/services/api-uploads/file.resolver.ts new file mode 100644 index 0000000..d51a6b3 --- /dev/null +++ b/services/api-uploads/file.resolver.ts @@ -0,0 +1,59 @@ +import { Resolver, Query, Args, Authorized, Ctx } from 'type-graphql'; +import { Mutation, Arg } from 'type-graphql'; + +import { getOneGeneric, getManyGenericWithArgs } from '@seed/graphql/BaseService'; +import { ApolloContext } from '@seed/interfaces/context'; + +import FileModel, { FileArgs, NewFileInput } from '@services/api-uploads/file.model'; +import { uploadFileWithBase64 } from '@services/api-uploads/services/UploadService'; + +@Resolver(FileModel) +export default class FileResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + @Query(() => FileModel) + @Authorized('admin') + async file(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new FileModel(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [FileModel]) + @Authorized('admin') + async files(@Args() fileArgs: FileArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new FileModel(); + return getManyGenericWithArgs(model, fileArgs, ctx); + } + + @Query(() => [FileModel]) + @Authorized() + async myFiles(@Args() fileArgs: FileArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new FileModel(); + const args = { ...fileArgs, accountId: ctx.ctx.user._id }; + return getManyGenericWithArgs(model, args, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => FileModel) + @Authorized() + async uploadFile(@Arg('input') input: NewFileInput, @Ctx() ctx: ApolloContext): Promise { + try { + return await uploadFileWithBase64(input.base64, input.title, input.privateFile || false, ctx); + } catch (error) { + throw error; + } + } +} diff --git a/services/api-uploads/serverless-upload.yaml b/services/api-uploads/serverless-upload.yaml new file mode 100644 index 0000000..855575f --- /dev/null +++ b/services/api-uploads/serverless-upload.yaml @@ -0,0 +1,40 @@ +app: ${file(./package.json):name} +service: uploads +package: + individually: true +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + S3_BUCKET: ${self:app}-uploads-production + iamRoleStatements: + - Effect: Allow + Action: + - s3:* + Resource: arn:aws:s3:::${self:provider.environment.S3_BUCKET}/* +plugins: + - serverless-webpack + - serverless-domain-manager +custom: + stage: ${opt:stage, self:provider.stage} + domains: ${file(./config/domains.yaml):domains} + customDomain: ${file(./config/domains.yaml):customDomain} + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk +functions: + fileHandler: + handler: handler.filesHandler + timeout: 30 # optional, in seconds, default is 6timeout: 10 # optional, in seconds, default is 6 + events: + - http: + path: / + method: post + cors: true + - http: + path: / + method: get + cors: true diff --git a/services/api-uploads/services/ImageService.ts b/services/api-uploads/services/ImageService.ts new file mode 100644 index 0000000..dffc5b3 --- /dev/null +++ b/services/api-uploads/services/ImageService.ts @@ -0,0 +1,53 @@ +import Jimp from 'jimp'; + +export const resize = async ( + input: Buffer, + mimeType: string, +): Promise<{ + // extension: string; + // mimeType: string; + largeBuffer: Buffer; + mediumBuffer: Buffer; + smallBuffer: Buffer; +}> => { + const resolutions = { + large: parseInt(process.env.IMG_largeRes || '2000'), + medium: parseInt(process.env.IMG_mediumRes || '1200'), + small: parseInt(process.env.IMG_smallRes || '600'), + }; + + const quality = parseInt(process.env.IMG_quality || '80'); + + try { + const jimpLarge = await Jimp.read(input); + const jimpMedium = await jimpLarge.clone(); + const jimpSmall = await jimpLarge.clone(); + + if (jimpLarge.getWidth() > resolutions.large) { + jimpLarge.resize(resolutions.large, Jimp.AUTO); + } + const largeBuffer = jimpLarge.quality(quality).getBufferAsync(mimeType); + + if (jimpMedium.getWidth() > resolutions.medium) { + jimpMedium.resize(resolutions.medium, Jimp.AUTO); + } + const mediumBuffer = jimpMedium.quality(quality).getBufferAsync(mimeType); + + if (jimpSmall.getWidth() > resolutions.small) { + jimpSmall.resize(resolutions.small, Jimp.AUTO); + } + const smallBuffer = jimpSmall.quality(quality).getBufferAsync(mimeType); + + const result = { + // extension: extension, + // mimeType: mimeType, + largeBuffer: await largeBuffer, + mediumBuffer: await mediumBuffer, + smallBuffer: await smallBuffer, + }; + + return result; + } catch (error) { + throw error; + } +}; diff --git a/services/api-uploads/services/S3Service.ts b/services/api-uploads/services/S3Service.ts new file mode 100644 index 0000000..5a44330 --- /dev/null +++ b/services/api-uploads/services/S3Service.ts @@ -0,0 +1,57 @@ +import AWS from 'aws-sdk'; +const s3 = new AWS.S3(); + +// eslint-disable-next-line @typescript-eslint/class-name-casing +export interface UploadParams { + fileName: string; + contentType: string; + buffer: Buffer | string; + bucket?: string; + acl?: boolean; +} + +export interface SignedUrlParams { + fileName: string; + bucket?: string; +} + +export const uploadToS3 = async (param: UploadParams): Promise => { + const bucket = param.bucket || process.env.S3_BUCKET || 'app-uploads'; + const region = process.env.awsRegion || 'eu-east-1'; + + AWS.config.update({ region: region }); + + const s3Params: AWS.S3.PutObjectRequest = { + Body: param.buffer, + Bucket: bucket, + Key: param.fileName, + ContentType: param.contentType, + ACL: param.acl ? 'private' : 'public-read', + }; + try { + const uploadResult = await s3.upload(s3Params).promise(); + + return uploadResult.Location; //`https://${bucket}.s3-${region}.amazonaws.com/${fileName}`; + } catch (error) { + throw error; + } +}; + +export const getSignedS3Url = async (param: SignedUrlParams, signedUrlExpireSeconds?: number): Promise => { + const bucket = param.bucket || process.env.S3_BUCKET || 'app-uploads'; + const region = process.env.awsRegion || 'eu-east-1'; + + AWS.config.update({ region: region }); + + try { + const url = s3.getSignedUrl('getObject', { + Bucket: bucket, + Key: param.fileName, + Expires: signedUrlExpireSeconds || 60 * 5, + }); + + return url; + } catch (error) { + throw error; + } +}; diff --git a/services/api-uploads/services/UploadService.ts b/services/api-uploads/services/UploadService.ts new file mode 100644 index 0000000..c0f440a --- /dev/null +++ b/services/api-uploads/services/UploadService.ts @@ -0,0 +1,133 @@ +import { v4 as uuid } from 'uuid'; + +import FileTypeHelper from 'file-type'; +import core from 'file-type/core'; + +import { uploadToS3 } from '@services/api-uploads/services/S3Service'; +import { resize } from '@services/api-uploads/services/ImageService'; + +import FileModel, { FileType } from '@services/api-uploads/file.model'; +import { ApolloContext } from '@seed/interfaces/context'; + +export const uploadFileWithBuffer = async ( + buffer: Buffer, + contentType: { mime: string; ext: string }, + title: string, + privateFile: boolean, + ctx: ApolloContext, +): Promise => { + try { + const _id = uuid(); + + const fileName = title + '-' + _id + '.' + contentType?.ext; + + const large = uploadToS3({ + acl: privateFile, + buffer: buffer, + contentType: contentType?.mime || '', + fileName, + }); + + const file = await new FileModel().saveOne( + { + _id, + title, + fileType: FileType.image, + accountId: ctx.ctx.user._id, + large: await large, + fileName, + }, + ctx, + ); + return file; + } catch (error) { + throw error; + } +}; + +export const uploadImageWithBuffer = async ( + buffer: Buffer, + contentType: { mime: string; ext: string }, + title: string, + privateFile: boolean, + ctx: ApolloContext, +): Promise => { + try { + const allImages = await resize(buffer, contentType.mime); + const _id = uuid(); + const fileName = title + '-' + _id + '.' + contentType?.ext; + const fileNameMedium = title + '-' + _id + '-medium.' + contentType?.ext; + const fileNameSmall = title + '-' + _id + '-small.' + contentType?.ext; + + const large = uploadToS3({ + acl: privateFile, + buffer: allImages.largeBuffer, + contentType: contentType?.mime, + fileName, + }); + const medium = uploadToS3({ + acl: privateFile, + buffer: allImages.mediumBuffer, + contentType: contentType?.mime, + fileName: fileNameMedium, + }); + const small = uploadToS3({ + acl: privateFile, + buffer: allImages.smallBuffer, + contentType: contentType?.mime, + fileName: fileNameSmall, + }); + + const file = await new FileModel().saveOne( + { + _id, + title, + fileType: FileType.image, + accountId: ctx.ctx.user._id, + large: await large, + medium: await medium, + small: await small, + fileName, + fileNameMedium, + fileNameSmall, + }, + ctx, + ); + return file; + } catch (error) { + throw error; + } +}; + +export const uploadFileWithBase64 = async (base64: string, title: string, privateFile: boolean, ctx: ApolloContext): Promise => { + try { + const cleanBase64 = base64.split('base64,').pop() || ''; + const buffer = Buffer.from(cleanBase64, 'base64'); + const contentTypeCheck = await FileTypeHelper.fromBuffer(buffer); + + const contentType = { + mime: '', + ext: '', + }; + + // Check extensions to deal with SVG + const extension = title.split('.').pop(); + if (extension == 'svg') { + contentType.mime = 'image/svg+xml'; + contentType.ext = 'svg'; + } else { + if (!contentTypeCheck?.mime) throw new Error('cannotReadExtensions'); + contentType.mime = contentTypeCheck.mime; + contentType.ext = contentTypeCheck.ext; + } + + const allowedMime = process.env.FILE_allowedMime || 'image/jpeg,image/png,image/gif,application/pdf'; + if (!allowedMime.split(',').includes(contentType.mime)) throw new Error('badExtensions'); + + if (contentType.mime.includes('image') && contentType.mime != 'image/svg+xml') + return await uploadImageWithBuffer(buffer, contentType, title, privateFile, ctx); + else return await uploadFileWithBuffer(buffer, contentType, title, privateFile, ctx); + } catch (error) { + throw error; + } +}; diff --git a/services/module-accounts/.gitignore b/services/module-accounts/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/services/module-accounts/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/services/module-accounts/README.md b/services/module-accounts/README.md new file mode 100644 index 0000000..a887982 --- /dev/null +++ b/services/module-accounts/README.md @@ -0,0 +1,19 @@ +# Make-it Account Module + +[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com) + +![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) + +### Description + +Adds the basic user management function based on + +- @src/accounts/account.model +- @src/accounts/account.components; + +### Install + +extend the resolvers + +export default class AccountAdminResolver extends AccountAdminBaseResolver +export default class AccountResolver extends AccountBaseResolver diff --git a/services/module-accounts/__tests/accounts.test.ts b/services/module-accounts/__tests/accounts.test.ts new file mode 100644 index 0000000..9d2db10 --- /dev/null +++ b/services/module-accounts/__tests/accounts.test.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { clog } from '../../../lib/seed/helpers/Utils'; +import { thunder } from '../../../__tests/app/config'; + +export class AccountEngineTest { + /* LOGIN */ + logInMagicLink = async (input: { email?: string }) => { + const { email } = input; + + const logInMagicLink = await thunder.query({ + logInMagicLink: [ + { + input: { email: email || 'software@makeit-studio.com' }, + }, + { + message: true, + }, + ], + }); + return logInMagicLink; + }; + + logInEmailCodeRequest = async (input: { email?: string }) => { + const { email } = input; + const logInEmailCodeRequest = await thunder.query({ + logInEmailCodeRequest: [ + { + input: { email: email || 'software@makeit-studio.com' }, + }, + { + message: true, + }, + ], + }); + + return logInEmailCodeRequest; + }; + + logInEmailCodeConfirm = async (input: { email?: string; code?: number }) => { + const { email, code } = input; + + const logInEmailCodeConfirm = await thunder.mutation({ + logInEmailCodeConfirm: [ + { + input: { email: email || 'software@makeit-studio.com', code: code || 0 }, + }, + { + idToken: true, + }, + ], + }); + + return logInEmailCodeConfirm; + }; + + /* SIGNIN */ + + signInEmailCodeRequest = async (input: { email?: string }) => { + const { email } = input; + + const signInEmailCodeRequest = await thunder.mutation({ + signInEmailCodeRequest: [ + { + input: { email: email || 'software@makeit-studio.com' }, + }, + { + message: true, + }, + ], + } as any); + + return signInEmailCodeRequest; + }; +} diff --git a/services/module-accounts/__tests/admin.playground.graphql b/services/module-accounts/__tests/admin.playground.graphql new file mode 100644 index 0000000..ccf1354 --- /dev/null +++ b/services/module-accounts/__tests/admin.playground.graphql @@ -0,0 +1,5 @@ +mutation resetPassword { + resetPassword(input: { email: "sanawar@makeit-studio.com" }) { + message + } +} diff --git a/services/module-accounts/__tests/playground.graphql b/services/module-accounts/__tests/playground.graphql new file mode 100644 index 0000000..bdd1420 --- /dev/null +++ b/services/module-accounts/__tests/playground.graphql @@ -0,0 +1,45 @@ +query login { + login(creds: { email: "", password: "" }) { + idToken + refreshToken + } +} + +query loginToken { + login(refreshToken: "") { + idToken + refreshToken + } +} + +mutation resetPassword { + resetPassword(input: { email: "sanawar@makeit-studio.com" }) { + message + } +} + +mutation updateMe { + updateMe(input: { settings: [{ type: notifications, email: true }, { type: promotions, sms: false, pushNotifications: true }] }) { + _id + firstName + lastName + settings { + type + email + pushNotifications + sms + } + } +} + +mutation signInEmailCodeRequest { + signInEmailCodeRequest(input: { email: "sanawar@makeit-studio.com" }) { + message + } +} + +mutation signInEmailCodeConfirmm { + signInEmailCodeConfirmm(input: { email: "sanawar@makeit-studio.com", code: 140773 }) { + idToken + } +} diff --git a/services/module-accounts/account.components.ts b/services/module-accounts/account.components.ts new file mode 100644 index 0000000..8136f4f --- /dev/null +++ b/services/module-accounts/account.components.ts @@ -0,0 +1,140 @@ +import { InputType, Field, ObjectType } from 'type-graphql'; +import { SettingsTypeEnum } from '@src/accounts/account.components'; +import { AvailableTranslation } from '@src/__components/components'; + +@InputType() +export class LoginInput { + @Field() + email: string; + @Field() + password: string; +} + +@InputType() +export class LinkEmailInput extends LoginInput { + @Field() + idToken: string; +} + +@InputType() +export class ResetPasswordInput { + @Field() + email: string; +} + +@InputType() +export class NewPasswordInput { + @Field() + oldPassword: string; + + @Field() + newPassword: string; + + @Field() + newPasswordConfirmation: string; +} + +@InputType() +export class NewEmailInput { + @Field() + newEmail: string; +} + +@InputType() +export class SMSVerifInput { + @Field() + email: string; + @Field() + emailCode: string; + @Field() + recaptchaToken: string; +} + +export class SMSVerif { + @Field() + phoneNumber: string; + @Field() + recaptchaToken: string; +} + +@InputType('SettingsInput') +@ObjectType() +export class SettingsComponent { + @Field(() => SettingsTypeEnum) + type: SettingsTypeEnum; + + @Field({ nullable: true }) + email?: boolean; + + @Field({ nullable: true }) + pushNotifications?: boolean; + + @Field({ nullable: true }) + sms?: boolean; +} + +@InputType() +export class CodeRequestInput { + @Field({ nullable: true }) + firstName?: string; + @Field({ nullable: true }) + lastName?: string; + @Field() + email: string; + + @Field({ nullable: true }) + phoneNumber?: string; + + @Field(() => AvailableTranslation, { nullable: true }) + language?: AvailableTranslation; +} + +@InputType() +export class CodeConfirmInput { + @Field() + email: string; + @Field() + code: number; +} + +@ObjectType() +export class CodeRequest { + @Field() + code: number; + @Field() + expiresAt: Date; +} + +// eslint-disable-next-line @typescript-eslint/class-name-casing +export class googleAuthToken { + refresh_token: string; + access_token: string; + scope: string; + token_type: string; + expiry_date: number; +} + +export class OAuthTokens { + googleTokens: googleAuthToken; +} + +export class SecurityStatusComponent { + emailVerified: boolean; + phoneVerified: boolean; +} + +export enum SecurityLevelEnum { + 'nothing' = 'nothing', // Nothing + 'email-only' = 'email-only', // Only Email + 'email-phone' = 'email-phone', // Email and phone + 'email-mfa' = 'email-mfa', // Email and other authenticator +} +// @ObjectType() +// export class ProfileCompletion { +// @Field() +// type: AccountTypeEnum; +// @Field() +// completed: boolean; +// @Field() +// currentStep: string; +// } diff --git a/services/module-accounts/account.model.ts b/services/module-accounts/account.model.ts new file mode 100644 index 0000000..4e18116 --- /dev/null +++ b/services/module-accounts/account.model.ts @@ -0,0 +1,258 @@ +import { ObjectType, Field, ID, ArgsType, Int, InputType } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import ImageComponent, { AccountGenderEnum } from '@seed/interfaces/components'; +import { AddressComponent } from '@seed/interfaces/components.geo'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { AvailableTranslation } from '@src/__components/components'; +import { GetArgs } from '@seed/graphql/Request'; +import { CodeRequest, OAuthTokens, SecurityLevelEnum, SecurityStatusComponent, SettingsComponent } from './account.components'; + +@ObjectType() +export default class BaseAccountModel extends BaseGraphModel { + public constructor(permissions: Permission) { + super({ + // ...init, + collectionName: 'accounts', + permissions: permissions, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field(() => [AccountTypeEnum]) + types: AccountTypeEnum[]; + + @Field(() => [String], { nullable: true }) + organisationIds?: string[]; + + @Field() + email: string; + + @Field({ nullable: true }) + userName?: string; + + @Field({ nullable: true }) + firstName?: string; + + @Field({ nullable: true }) + lastName?: string; + + @Field(() => ImageComponent, { nullable: true }) + profilePicture?: ImageComponent; + + @Field({ nullable: true }) + phoneNumber?: string; + + @Field(() => AccountGenderEnum, { nullable: true }) + gender?: AccountGenderEnum; + + // @Field(() => Int, { nullable: true }) + // age?: number; + + @Field({ nullable: true }) + birthDate?: Date; + + @Field(() => Int, { nullable: true }) + getAge(): number | undefined { + if (this.birthDate) { + const today = new Date(); + let age = today.getFullYear() - this.birthDate.getFullYear(); + const diff = today.getMonth() - this.birthDate.getMonth(); + if (diff < 0 || (diff === 0 && today.getDate() < this.birthDate.getDate())) { + age--; + } + return age; + } + } + + @Field(() => AddressComponent, { nullable: true }) + address?: AddressComponent; + + @Field(() => AvailableTranslation, { nullable: true }) + language: AvailableTranslation; + + @Field(() => [SettingsComponent], { nullable: true }) + settings?: SettingsComponent[]; + + /* + For Authorization codes + */ + authCodes?: CodeRequest[]; + /* + For OauthProvider codes + */ + oAuthTokens?: OAuthTokens; + + security?: SecurityStatusComponent; + @Field() + securityCheck(): boolean { + // Check the level of security for this app + const securityLevel: SecurityLevelEnum = (process.env.SECURITY_LEVEL as SecurityLevelEnum) || SecurityLevelEnum.nothing; + + switch (securityLevel) { + case SecurityLevelEnum.nothing: + console.error('[SECURITY] NO CONFIG'); + break; + case SecurityLevelEnum['email-only']: + if (this.security?.emailVerified) return true; + else return false; + case SecurityLevelEnum['email-phone']: + if (this.security?.emailVerified && this.security?.phoneVerified) return true; + else return false; + case SecurityLevelEnum['email-mfa']: + console.error('[SECURITY] NOT IMPLEMENTED'); + break; + default: + console.error('[SECURITY] BAD CONFIGURATION'); + break; + } + + return true; + } + + // [x: string]: any; + + searchOptions(): string[] { + return ['email', 'firstName', 'lastName', 'userName']; + } + + filterOptions(): string[] { + return ['email', 'firstName', 'lastName', 'userName']; + } +} + +@ArgsType() +export class BaseAccountArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; + + @Field(() => AccountTypeEnum) + types: AccountTypeEnum; +} + +@InputType() +export class NewAccountInputAdmin implements Partial { + @Field(() => AccountTypeEnum) + type: AccountTypeEnum; + + @Field(() => [String], { nullable: true }) + organisationIds?: string[]; + + @Field() + email: string; + + // @Field() + // password: string; + + // @Field() + // passwordConfirmation: string; + + @Field({ nullable: true }) + userName?: string; + + @Field({ nullable: true }) + firstName?: string; + + @Field({ nullable: true }) + lastName?: string; + + @Field(() => ImageComponent, { nullable: true }) + profilePicture?: ImageComponent; + + @Field({ nullable: true }) + phoneNumber?: string; + + @Field(() => AccountGenderEnum, { nullable: true }) + gender?: AccountGenderEnum; + + @Field({ nullable: true }) + birthDate?: Date; + + @Field(() => AddressComponent, { nullable: true }) + address?: AddressComponent; + @Field(() => AvailableTranslation, { nullable: true }) + language?: AvailableTranslation; + + @Field(() => [SettingsComponent], { nullable: true }) + settings?: SettingsComponent[]; +} + +@InputType() +export class NewBaseAccountInput implements Partial { + @Field() + email: string; + + @Field() + password: string; + + @Field() + passwordConfirmation: string; + + @Field({ nullable: true }) + userName?: string; + + @Field({ nullable: true }) + firstName?: string; + + @Field({ nullable: true }) + lastName?: string; + + @Field(() => ImageComponent, { nullable: true }) + profilePicture?: ImageComponent; + + @Field({ nullable: true }) + phoneNumber?: string; + + @Field(() => AccountGenderEnum, { nullable: true }) + gender?: AccountGenderEnum; + + @Field({ nullable: true }) + birthDate?: Date; + + @Field(() => AddressComponent, { nullable: true }) + address?: AddressComponent; + + @Field(() => AvailableTranslation, { nullable: true }) + language?: AvailableTranslation; + + @Field(() => [SettingsComponent], { nullable: true }) + settings?: SettingsComponent[]; +} + +@InputType() +export class EditBaseAccountInput implements Partial { + @Field({ nullable: true }) + userName?: string; + + @Field({ nullable: true }) + firstName?: string; + + @Field({ nullable: true }) + lastName?: string; + + @Field(() => ImageComponent, { nullable: true }) + profilePicture?: ImageComponent; + + @Field({ nullable: true }) + phoneNumber?: string; + + @Field(() => AccountGenderEnum, { nullable: true }) + gender?: AccountGenderEnum; + + @Field({ nullable: true }) + birthDate?: Date; + + @Field(() => AddressComponent, { nullable: true }) + address?: AddressComponent; + + @Field(() => AvailableTranslation, { nullable: true }) + language?: AvailableTranslation; + + @Field(() => [SettingsComponent], { nullable: true }) + settings?: SettingsComponent[]; +} diff --git a/services/module-accounts/account.resolver.admin.ts b/services/module-accounts/account.resolver.admin.ts new file mode 100644 index 0000000..2ae9a9d --- /dev/null +++ b/services/module-accounts/account.resolver.admin.ts @@ -0,0 +1,125 @@ +import { Resolver, Query, Args, Ctx, Authorized } from 'type-graphql'; +import { Mutation, Arg } from 'type-graphql'; + +import { ApolloContext } from '@seed/interfaces/context'; +import { getOneGeneric, editOneGeneric, getManyGenericWithArgs } from '@seed/graphql/BaseService'; + +import { deleteOneGeneric, signinGenericNoPassword } from './services/AccountService'; + +import AccountModel, { EditAccountInput, AccountArgs } from '@src/accounts/account.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { NewAccountInputAdmin } from './account.model'; +import { AvailableTranslation } from '@src/__components/components'; +import { FirebaseTokenResult, SimpleResult } from '@seed/interfaces/components'; +import { LoginInput, ResetPasswordInput } from './account.components'; +import Firebase from '@seed/services/auth/FirebaseService'; +import AccountBaseResolver from './account.resolver'; +import { sendEmail } from '@seed/services/email/EmailService'; +import FirebaseAuth from '@seed/services/auth/FirebaseAuthService'; +import { ApolloError } from 'apollo-server-lambda'; +import { newError } from '@seed/helpers/Error'; + +@Resolver(AccountModel) +export default class AccountAdminBaseResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + @Query(() => AccountModel) + @Authorized() + async me(@Ctx() ctx: ApolloContext): Promise { + return ctx.ctx.user as AccountModel; + } + + /* + * Auth side + */ + @Query(() => FirebaseTokenResult) + async login( + @Arg('creds', { nullable: true }) creds?: LoginInput, + @Arg('refreshToken', { nullable: true }) refreshToken?: string, + ): Promise { + if (creds) return FirebaseAuth.getInstance().loginWithEmailPassword(creds); + if (refreshToken) return FirebaseAuth.getInstance().loginWithRefreshToken(refreshToken); + + throw newError(3000); + } + + @Query(() => AccountModel) + @Authorized(AccountTypeEnum.admin) + async accountsGetOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new AccountModel(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [AccountModel]) + @Authorized(AccountTypeEnum.admin) + async accountsGetMany(@Args() accountArgs: AccountArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new AccountModel(); + return getManyGenericWithArgs(model, accountArgs, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + /* + * Admin side + */ + + @Mutation(() => AccountModel) + @Authorized(AccountTypeEnum.admin) + async accountsAddOne(@Arg('input') input: NewAccountInputAdmin, @Ctx() ctx: ApolloContext): Promise { + const model = new AccountModel(); + const result = await signinGenericNoPassword(model, input.type, input, ctx); + const resetPasswordLink = await Firebase.getInstance().getResetPasswordLink(result.email); + + await sendEmail('adminAccountAddOne', result.language || AvailableTranslation.en, result.email, { ...result, resetPasswordLink }); + + return result as any; + } + + @Mutation(() => AccountModel) + @Authorized(AccountTypeEnum.admin) + async accountsEditOne(@Arg('id') id: string, @Arg('input') input: EditAccountInput, @Ctx() ctx: ApolloContext): Promise { + const model = new AccountModel(); + return editOneGeneric(model, id, input, ctx); + } + + @Mutation(() => AccountModel) + @Authorized(AccountTypeEnum.admin) + async accountsDeleteOne( + @Arg('id') id: string, + @Arg('userType', () => AccountTypeEnum) userType: AccountTypeEnum, + @Ctx() ctx: ApolloContext, + ): Promise { + const model = new AccountModel(); + return deleteOneGeneric(model, userType, id, ctx); + } + + /* + * ACTIONS + */ + @Mutation(() => SimpleResult) + async resetPassword(@Arg('input') input: ResetPasswordInput, @Ctx() ctx: ApolloContext): Promise { + return new AccountBaseResolver().resetPassword(input, ctx); + } +} diff --git a/services/module-accounts/account.resolver.ts b/services/module-accounts/account.resolver.ts new file mode 100644 index 0000000..fcadb10 --- /dev/null +++ b/services/module-accounts/account.resolver.ts @@ -0,0 +1,171 @@ +import { ApolloError } from 'apollo-server-lambda'; +import { Resolver, Query, Ctx, Authorized, Mutation, Arg } from 'type-graphql'; + +import { ApolloContext } from '@seed/interfaces/context'; +import { editOneGeneric } from '@seed/graphql/BaseService'; + +import { signinAnonymous, signinGeneric } from './services/AccountService'; + +import AccountModel, { EditAccountInput } from '@src/accounts/account.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { FirebaseTokenResult, SimpleResult } from '@seed/interfaces/components'; +import { ResetPasswordInput, NewEmailInput, NewPasswordInput, LoginInput, LinkEmailInput } from './account.components'; +import Firebase from '@seed/services/auth/FirebaseService'; +import { sendEmail } from '@seed/services/email/EmailService'; +import { AvailableTranslation } from '@src/__components/components'; +import FirebaseAuth from '@seed/services/auth/FirebaseAuthService'; +import { newError } from '@seed/helpers/Error'; + +@Resolver(AccountModel) +export default class AccountBaseResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + @Query(() => AccountModel) + @Authorized() + async me(@Ctx() ctx: ApolloContext): Promise { + return ctx.ctx.user as AccountModel; + } + + /* + * Auth side + */ + @Query(() => FirebaseTokenResult) + async login( + @Ctx() ctx: ApolloContext, + @Arg('creds', { nullable: true }) creds?: LoginInput, + @Arg('refreshToken', { nullable: true }) refreshToken?: string, + ): Promise { + if (creds) { + const account = new AccountModel(); + await account.getOne({ email: creds.email }, null); + + return FirebaseAuth.getInstance().loginWithEmailPassword(creds); + } + if (refreshToken) return FirebaseAuth.getInstance().loginWithRefreshToken(refreshToken); + + return signinAnonymous(ctx); + } + + @Query(() => SimpleResult) + async magicLink(@Arg('input') input: ResetPasswordInput, @Ctx() ctx: ApolloContext): Promise { + const account = new AccountModel(); + + try { + await account.getOne({ email: input.email }, null); + const magicLink = await Firebase.getInstance().getMagicLink(input.email); + + await sendEmail('magicLink', account.language, account.email, { ...account, magicLink }); + } catch (error) { + throw error; + } + + return { message: 'sent' }; + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + /* + * Account side + */ + + // @Mutation(() => AccountModel) + // async register(@Arg('input') input: NewAccountInput, @Ctx() ctx: ApolloContext): Promise { + // const model = new AccountModel(); + // return signinGeneric(model, AccountTypeEnum.user, input, ctx); + // } + + @Mutation(() => AccountModel) + @Authorized() + async updateMe(@Arg('input') input: EditAccountInput, @Ctx() ctx: ApolloContext): Promise { + const model = new AccountModel(); + return editOneGeneric(model, ctx.ctx.user._id, input, ctx); + } + + @Mutation(() => AccountModel) + @Authorized() + async updateMeEmail(@Arg('input') input: NewEmailInput, @Ctx() ctx: ApolloContext): Promise { + const currentUser = ctx.ctx.user as AccountModel; + try { + await Firebase.getInstance().updateEmail({ uid: currentUser._id, email: input.newEmail }); + return editOneGeneric(currentUser, currentUser._id, { email: input.newEmail }, ctx); + } catch (error) { + throw error; + } + } + + @Mutation(() => AccountModel) + @Authorized() + async updateMePassword(@Arg('input') input: NewPasswordInput, @Ctx() ctx: ApolloContext): Promise { + const currentUser = ctx.ctx.user as AccountModel; + + if (input.newPassword != input.newPasswordConfirmation) throw newError(2006); + + try { + // Check old password + FirebaseAuth.getInstance().loginWithEmailPassword({ + email: currentUser.email, + password: input.oldPassword, + }); + } catch (error) { + throw newError(20060); + } + + try { + await Firebase.getInstance().updatePassword({ uid: currentUser._id, password: input.newPassword }); + return currentUser; + } catch (error) { + throw error; + } + } + + @Mutation(() => SimpleResult) + async resetPassword(@Arg('input') input: ResetPasswordInput, @Ctx() ctx: ApolloContext): Promise { + const account = new AccountModel(); + + try { + await account.getOne({ email: input.email }, null); + const resetPasswordLink = await Firebase.getInstance().getResetPasswordLink(input.email); + + await sendEmail('resetPassword', account.language, account.email, { ...account, resetPasswordLink }); + } catch (error) { + throw error; + } + + return { message: 'sent' }; + } + + @Mutation(() => AccountModel) + @Authorized('public') + async registerGuest( + @Arg('input') input: LinkEmailInput, + @Arg('otherInfo') otherInfo: EditAccountInput, + @Ctx() ctx: ApolloContext, + ): Promise { + const account = new AccountModel(); + + const result = await FirebaseAuth.getInstance().linkWithEmailPassword(input); + return editOneGeneric(account, result.localId, { ...otherInfo, email: result.email, types: [AccountTypeEnum.user] }, ctx); + } +} diff --git a/services/module-accounts/env/emails.settings.import.json b/services/module-accounts/env/emails.settings.import.json new file mode 100644 index 0000000..1d000a7 --- /dev/null +++ b/services/module-accounts/env/emails.settings.import.json @@ -0,0 +1,229 @@ + + +/* +* EMAILS +*/ + +{ + "_id" : "04e48fa2-714f-4d7f-ae17-c791ccf6e5bb", + "key" : "resetPassword", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "You requested a new password", + "fr" : "Vous avez demandé un nouveau mot de passe", + "nl" : "You requested a new password" + }, + "body" : { + "en" : "

Here is the password reset link : ${resetPasswordLink}

", + "fr" : "

Voici le lien pour réinitialiser votre mot de passe : ${resetPasswordLink}

", + "nl" : "

Here is the password reset link : ${resetPasswordLink}

" + }, + "availableFields" : [ + "firstName", + "lastName", + "email", + "resetPasswordLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "0f245c6c-e39a-465f-9374-e2268a0cbf83", + "key" : "adminAccountAddOne", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "The administrator of [PROJECT] created an account for you", + "fr" : "L'administrateur de [PROJECT] a crée un compte pour vous!", + "nl" : "The administrator of [PROJECT] created an account for you" + }, + "body" : { + "en" : "

Please setup your password by clicking this link : ${resetPasswordLink}

", + "fr" : "

Pouvez-vous configurer un mot de passe en cliquant sur ce lien ? : ${resetPasswordLink}

", + "nl" : "

Please setup your password by clicking this link : ${resetPasswordLink}

" + }, + "availableFields" : [ + "firstName", + "lastName", + "email", + "resetPasswordLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "16496dfc-5935-495b-a352-52efe158d55c", + "key" : "organisationsAddOneAccount", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "The administrator of [ORG] created an account for you", + "fr" : "L'administrateur de l'organisation [ORG] a crée un compte pour vous!", + "nl" : "The administrator of [ORG] created an account for you" + }, + "body" : { + "en" : "

Please setup your password by clicking this link : ${resetPasswordLink}

", + "fr" : "

Pouvez-vous configurer un mot de passe en cliquant sur ce lien ? : ${resetPasswordLink}

", + "nl" : "

Please setup your password by clicking this link : ${resetPasswordLink}

" + }, + "availableFields" : [ + "firstName", + "lastName", + "email", + "resetPasswordLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "16496dfc-5875-495b-axx2-52efe158d55c", + "key" : "emailCodeSignIn", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "subject" : { + "en" : "Here is your code to login : ${data.code}", + "fr" : "Voici votre code d'accès: ${data.code}", + "nl" : "Here is your code to login : ${data.code}" + }, + "body" : { + "en" : "Here is your code to login : ${data.code}", + "fr" : "Voici votre code d'accès: ${data.code}", + "nl" : "Here is your code to login : ${data.code}" + }, + "availableFields" : [ + "firstName", + "code" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +}, +{ + "_id" : "04e48fa2-714f-4d7f-ae17-c791ccf6e5bb", + "key" : "magicLink", + "templateId" : { + "en" : "", + "fr" : "", + "nl" : "" + }, + "subject" : { + "en" : "Your magic link is ready", + "fr" : "Votre demande de lien d'accès", + "nl" : "Your magic link is ready" + }, + "body" : { + "en" : "Here is your link to login : ${magicLink}", + "fr" : "Voici votre lien d'accès: ${magicLink}", + "nl" : "Here is your link to login : ${magicLink}" + }, + "fromEmail" : "no-reply@makeit-studio.com", + "replyToEmail" : "software@makeit-studio.com", + "fromName" : "Make it | Studio", + "availableFields" : [ + "firstName", + "lastName", + "email", + "magicLink" + ], + "custom" : false, + "type" : "emails", + "cci" : [ + + ], + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ], + "createdAt" : ISODate(), + "updatedAt" : ISODate(), +} + +MAGIC_LINK_URL + +IOS_PACKAGE_NAME +ANDROID_PACKAGE_NAME \ No newline at end of file diff --git a/services/module-accounts/env/env.settings.import.json b/services/module-accounts/env/env.settings.import.json new file mode 100644 index 0000000..6f69cc8 --- /dev/null +++ b/services/module-accounts/env/env.settings.import.json @@ -0,0 +1,51 @@ +{ + "_id" : "44ae9d0c-75d5-4c4f-8a74-020104b3d0d5", + "key" : "MAGIC_LINK_URL", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "987cd596-a364-4d84-a423-18f549595cf6", + "key" : "IOS_PACKAGE_NAME", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "d" : [ + "admin", + ], + "r" : [ + "admin", + ], + "w" : [ + "admin", + ] +}, +{ + "_id" : "b48d4e97-ddcf-4b7b-89fd-d7844071563f", + "key" : "ANDROID_PACKAGE_NAME", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, \ No newline at end of file diff --git a/services/module-accounts/functions/auth-history/pages.model.ts b/services/module-accounts/functions/auth-history/pages.model.ts new file mode 100644 index 0000000..5203a81 --- /dev/null +++ b/services/module-accounts/functions/auth-history/pages.model.ts @@ -0,0 +1,51 @@ +// import { ObjectType, Field, ID, ArgsType, InputType, registerEnumType, Int } from 'type-graphql'; + +// import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +// import { Permission } from '@seed/interfaces/permission'; + +// import { GetArgs } from '@seed/graphql/Request'; + +// import { GraphQLJSONObject } from 'graphql-type-json'; +// import { AccountTypeEnum } from '@src/accounts/account.components'; +// import { EngineModel } from '@seed/engine/EngineModel'; +// import { PageDBInterfaceSchema, PageDBSchema, PageSchema } from './schemas/auth-history.schema'; +// import { plainToClass } from 'class-transformer'; +// import { IEngineSchema } from '@seed/engine/EngineSchema'; +// import { uploadToS3 } from '@services/api-uploads/services/S3Service'; +// import { updateCaching } from '@services/module-cms/services/PageService'; +// import { StreamDBSchema } from '@seed/engine/utils/streams/schemas/stream.schema'; + +// const permissions: Permission = { +// c: [AccountTypeEnum.admin], +// r: [AccountTypeEnum.public], +// w: [AccountTypeEnum.admin], +// d: [AccountTypeEnum.admin], +// }; + +// @ObjectType() +// export default class PageModel extends EngineModel { +// public constructor(input?: PageDBInterfaceSchema & Partial) { +// const dataInit = plainToClass(PageDBSchema, input || {}); +// super({ +// collectionName: 'pages', +// permissions: permissions, +// dataInit, +// }); +// } + +// plainToClass(plain: any): PageSchema | PageSchema[] { +// return plainToClass(PageSchema, plain); +// } + +// async afterCreate(): Promise { +// await updateCaching(this); +// } + +// async afterUpdate(): Promise { +// await updateCaching(this); +// } + +// async afterDelete(): Promise { +// await updateCaching(this); +// } +// } diff --git a/services/module-accounts/functions/auth-history/pages.resolver.admin.ts b/services/module-accounts/functions/auth-history/pages.resolver.admin.ts new file mode 100644 index 0000000..9722872 --- /dev/null +++ b/services/module-accounts/functions/auth-history/pages.resolver.admin.ts @@ -0,0 +1,72 @@ +// import { Resolver, Query, Args, Authorized, Ctx } from 'type-graphql'; +// import { Mutation, Arg } from 'type-graphql'; + +// import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric } from '@seed/graphql/BaseService'; + +// import { AccountTypeEnum } from '@src/accounts/account.components'; +// import { createEngineMutationResolver, createEngineQueryResolver } from '@seed/engine/genericResolvers/BaseEngineResolver'; +// import { PageSchema } from './schemas/auth-history.schema'; +// import { PageArgs, NewPageInput, EditPageInput } from './schemas/pages.input'; +// import PageModel from './pages.model'; +// import { SuccessResponse } from '@seed/interfaces/response'; +// import { EngineMiddleware } from '@seed/graphql/MiddlewareV2'; +// import { updateCaching as updatePagesCaching } from '@services/module-cms/services/PageService'; +// import { ApolloContext } from '@seed/interfaces/context'; + +// const PageBaseQueryAdminResolver = createEngineQueryResolver({ +// domain: 'pages', +// schemaName: PageSchema, +// modelName: PageModel, +// argsType: PageArgs, +// engineMiddleware: { +// authorization: [AccountTypeEnum.admin], +// }, +// }); + +// const PageBaseMutationAdminResolver = createEngineMutationResolver({ +// domain: 'pages', +// schemaName: PageSchema, +// modelName: PageModel, +// newInput: NewPageInput, +// editInput: EditPageInput, +// engineMiddleware: { +// authorization: [AccountTypeEnum.admin], +// }, +// }); + +// @Resolver(PageModel) +// export class PageQueryAdminResolver extends PageBaseQueryAdminResolver { +// /* +// ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ +// ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ +// ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ +// ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ +// ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ +// ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ +// */ +// } + +// @Resolver(PageModel) +// export class PageMutationAdminResolver extends PageBaseMutationAdminResolver { +// /* +// ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ +// ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ +// ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ +// ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ +// ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ +// ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ +// */ + +// @Mutation(() => SuccessResponse) +// @EngineMiddleware({ +// authorization: [AccountTypeEnum.admin], +// }) +// async refreshCaching(@Ctx() ctx: ApolloContext): Promise { +// const model = new PageModel(); +// await updatePagesCaching(model); + +// return { success: true }; +// } +// } + +// export const PageAdminResolvers = [PageQueryAdminResolver, PageMutationAdminResolver]; diff --git a/services/module-accounts/functions/auth-history/schemas/auth-history.schema.ts b/services/module-accounts/functions/auth-history/schemas/auth-history.schema.ts new file mode 100644 index 0000000..9c8d03c --- /dev/null +++ b/services/module-accounts/functions/auth-history/schemas/auth-history.schema.ts @@ -0,0 +1,45 @@ +import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema'; +import { GraphQLJSONObject } from 'graphql-type-json'; +import { InputType, ObjectType, Field, Int } from 'type-graphql'; + +export enum AuthCodeType { + 'verification' = 'verification', + 'authentication' = 'authentication', +} + +export enum AuthCanalType { + 'email' = 'email', + 'phone' = 'phone', + 'authenticator' = 'authenticator', +} + +@ObjectType() +export class AuthHistoryBaseSchema { + @Field() + code: string; + + @Field(() => AuthCodeType) + type: AuthCodeType; + + @Field(() => AuthCanalType) + canal: AuthCanalType; +} + +@ObjectType() +export class AuthHistoryIDBSchema extends AuthHistoryBaseSchema { + @Field() + accountId: string; +} + +@ObjectType() +export class AuthHistoryDBSchema extends AuthHistoryIDBSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class AuthHistorySchema extends AuthHistoryDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/services/module-accounts/functions/auth-history/schemas/pages.input.ts b/services/module-accounts/functions/auth-history/schemas/pages.input.ts new file mode 100644 index 0000000..755f62c --- /dev/null +++ b/services/module-accounts/functions/auth-history/schemas/pages.input.ts @@ -0,0 +1,16 @@ +// import { InputType, ArgsType, Field } from 'type-graphql'; + +// import { GetManyArgs } from '@seed/graphql/Request'; +// import { PageBaseSchema } from './auth-history.schema'; + +// @InputType() +// export class NewPageInput extends PageBaseSchema {} + +// @InputType() +// export class EditPageInput extends PageBaseSchema { +// @Field({ nullable: true }) +// title: string; +// } + +// @ArgsType() +// export class PageArgs extends GetManyArgs {} diff --git a/services/module-accounts/resolvers/auth/account.auth.mfa.resolver.ts b/services/module-accounts/resolvers/auth/account.auth.mfa.resolver.ts new file mode 100644 index 0000000..4738915 --- /dev/null +++ b/services/module-accounts/resolvers/auth/account.auth.mfa.resolver.ts @@ -0,0 +1,246 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql'; +import _ from 'lodash'; +import { v4 as uuid } from 'uuid'; + +import AccountModel from '@src/accounts/account.model'; +import { CodeConfirmInput, CodeRequestInput, SMSVerif, SMSVerifInput } from '@services/module-accounts/account.components'; +import Firebase from '@lib/seed/services/auth/FirebaseService'; +import { sendEmail } from '@lib/seed/services/email/EmailService'; +import { FirebaseTokenResult, SimpleResult } from '@lib/seed/interfaces/components'; +import moment from 'moment'; +import { generateCodeNumber } from '@seed/helpers/Utils'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ApolloContext } from '@seed/interfaces/context'; +import { newError } from '@seed/helpers/Error'; +import { AvailableTranslation } from '@src/__components/components'; +import { DateTime } from 'luxon'; +import FirebaseAuth from '@seed/services/auth/FirebaseAuthService'; +import { SuccessResponse } from '@seed/interfaces/response'; + +// TODO - Handle change emails & change phone number + +@Resolver(AccountModel) +export class AccountQueryMFAAuthResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + @Query(() => SimpleResult) + async sendEmailVerificationCode(@Arg('input') input: CodeRequestInput, @Ctx() ctx: ApolloContext): Promise { + try { + const magicLink = await Firebase.getInstance().getMagicLink(input.email); + await sendEmail('magicLinkLogIn', input.language || AvailableTranslation.en, input.email, { ...input, magicLink }); + return { message: 'sent' }; + } catch (error) { + throw error; + } + } + + @Query(() => SimpleResult) + async sendPhoneVerificationCode(@Arg('input') input: SMSVerifInput): Promise { + try { + // Check if account exists + + // Get Phone Number - TODO : google-libphonenumber + const phoneNumber = '+32484740444'; + // Get Code + + return await FirebaseAuth.getInstance().sendSMSVerification({ phoneNumber, recaptchaToken: input.recaptchaToken }); + } catch (error) { + throw error; + } + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + + */ + + @Mutation(() => FirebaseTokenResult) + async logInEmailCodeConfirm(@Arg('input') input: CodeConfirmInput): Promise { + const { code, email } = input; + + try { + // Check if exists on firebase already + const { accountCheck, model, firebaseUserData } = await getAccount(email); + // Get the codes + const codes = accountCheck.authCodes; + + // verify the code + const currentCode = _.find(codes, { code: code }); + if (!currentCode) throw newError(2300); + + // verify the expiration date + if (DateTime.utc() >= DateTime.fromJSDate(currentCode.expiresAt).toUTC()) throw newError(2301); + + // Get the token from firebase + const token = await Firebase.getInstance().createTokenId(email); + + // remove it from the array + await model.updateOneCustom( + { _id: firebaseUserData.uid }, + { + $pull: { authCodes: { code: code } }, + }, + null, + ); + return token; + } catch (error) { + throw error; + } + } +} + +@Resolver(AccountModel) +export class AccountMutationCodeAuthResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + + */ +} + +async function getCode(model: AccountModel, data: AccountModel) { + // Get the codes + let codes = data.authCodes; + if (codes) { + // Remove the expired ones + _.remove(codes, (e) => { + const diffInMs = DateTime.fromJSDate(e.expiresAt).diffNow('milliseconds').milliseconds; + const MAX_TIMEOUT = 15 * 60 * 1000; + + if (diffInMs >= MAX_TIMEOUT) return true; + + return false; + }); + } else codes = []; + + // Check if too many attempt in the last 15 minutes + if (codes.length > 5) throw newError(2302); + + // Add the new one + const code = generateCodeNumber(); + + codes.push({ + code: code, + expiresAt: DateTime.utc() + .plus({ minutes: 15 }) + .toJSDate(), + }); + // Save in DB + await model.updateOneCustom( + { _id: data._id }, + { + $set: { authCodes: codes }, + }, + + null, + ); + return code; +} + +async function getAccount(email: string) { + let firebaseUserData; + try { + firebaseUserData = await Firebase.getInstance().getUserByEmail(email); + } catch (error) { + throw newError(2004, error); + } + + // Check if in DB or not + const model = new AccountModel(); + + const accountCheck = await (await model.db()).findOne({ _id: firebaseUserData.uid }); + // Verify if has thinkific signin + if (!accountCheck) throw newError(2004); + return { accountCheck, model, firebaseUserData }; +} + +async function createAccount(input: CodeRequestInput) { + let firebaseUserData; + try { + firebaseUserData = await Firebase.getInstance().getUserByEmail(input.email); + } catch (error) { + try { + firebaseUserData = await Firebase.getInstance().createUser({ + email: input.email, + password: uuid(), + }); + } catch (error) { + throw error; + } + } + + const dataToSave = { + ...input, + firebaseData: { + aud: firebaseUserData.aud, + emailVerified: firebaseUserData.email_verified, + firebase: JSON.stringify(firebaseUserData.firebase), + }, + updatedAt: new Date(), + }; + + // Check if in DB or not + const model = new AccountModel(); + const type = AccountTypeEnum.user; + + let account; + + const accountCheck = await (await model.db()).findOne({ _id: firebaseUserData.uid }); + // Verify if has thinkific signin + if (accountCheck) { + account = accountCheck; + await (await model.db()).updateOne( + { _id: firebaseUserData.uid }, + { + $set: { ...input }, + }, + ); + } else { + const types = [type]; + account = await model.saveOne( + { + ...dataToSave, + _id: firebaseUserData.uid, + types, + r: [AccountTypeEnum.admin, firebaseUserData.uid], + w: [AccountTypeEnum.admin, firebaseUserData.uid], + d: [AccountTypeEnum.admin, firebaseUserData.uid], + }, + null, + ); + } + return { model, firebaseUserData, account }; +} diff --git a/services/module-accounts/resolvers/auth/account.auth.resolver.ts b/services/module-accounts/resolvers/auth/account.auth.resolver.ts new file mode 100644 index 0000000..2d41d92 --- /dev/null +++ b/services/module-accounts/resolvers/auth/account.auth.resolver.ts @@ -0,0 +1,271 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql'; +import _ from 'lodash'; +import { v4 as uuid } from 'uuid'; + +import AccountModel from '@src/accounts/account.model'; +import { CodeConfirmInput, CodeRequestInput } from '@services/module-accounts/account.components'; +import Firebase from '@lib/seed/services/auth/FirebaseService'; +import { sendEmail } from '@lib/seed/services/email/EmailService'; +import { FirebaseTokenResult, SimpleResult } from '@lib/seed/interfaces/components'; +import moment from 'moment'; +import { generateCodeNumber } from '@seed/helpers/Utils'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ApolloContext } from '@seed/interfaces/context'; +import { newError } from '@seed/helpers/Error'; +import { AvailableTranslation } from '@src/__components/components'; +import { DateTime } from 'luxon'; + +@Resolver(AccountModel) +export class AccountQueryCodeAuthResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + @Query(() => SimpleResult) + async logInMagicLink(@Arg('input') input: CodeRequestInput, @Ctx() ctx: ApolloContext): Promise { + try { + const magicLink = await Firebase.getInstance().getMagicLink(input.email); + await sendEmail('magicLinkLogIn', input.language || AvailableTranslation.en, input.email, { ...input, magicLink }); + return { message: 'sent' }; + } catch (error) { + throw error; + } + } + + @Query(() => SimpleResult) + async logInEmailCodeRequest(@Arg('input') input: CodeRequestInput): Promise { + const { email } = input; + + try { + // Check if exists on firebase already + const { accountCheck, model } = await getAccount(email); + + // Add the codes to the account + const code = await getCode(model, accountCheck); + await sendEmail('emailCodeLogIn', accountCheck.language, input.email, { firstname: accountCheck.firstName || 'stranger', code }); + return { message: 'sent' }; + } catch (error) { + throw error; + } + } + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + + */ + + @Mutation(() => FirebaseTokenResult) + async logInEmailCodeConfirm(@Arg('input') input: CodeConfirmInput): Promise { + const { code, email } = input; + + try { + // Check if exists on firebase already + const { accountCheck, model, firebaseUserData } = await getAccount(email); + // Get the codes + const codes = accountCheck.authCodes; + + // verify the code + const currentCode = _.find(codes, { code: code }); + if (!currentCode) throw newError(2300); + + // verify the expiration date + if (DateTime.utc() >= DateTime.fromJSDate(currentCode.expiresAt).toUTC()) throw newError(2301); + + // Get the token from firebase + const token = await Firebase.getInstance().createTokenId(email); + + // remove it from the array + await model.updateOneCustom( + { _id: firebaseUserData.uid }, + { + $pull: { authCodes: { code: code } }, + }, + null, + ); + return token; + } catch (error) { + throw error; + } + } +} + +@Resolver(AccountModel) +export class AccountMutationCodeAuthResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + + */ + + @Mutation(() => SimpleResult) + async signInMagicLink(@Arg('input') input: CodeRequestInput, @Ctx() ctx: ApolloContext): Promise { + try { + const { account } = await createAccount(input); + + const magicLink = await Firebase.getInstance().getMagicLink(input.email); + await sendEmail('magicLinkSignIn', account.language, account.email, { ...account, magicLink }); + return { message: 'sent' }; + } catch (error) { + throw error; + } + } + + @Mutation(() => SimpleResult) + async signInEmailCodeRequest(@Arg('input') input: CodeRequestInput): Promise { + // Check if exists on firebase already + try { + const { model, firebaseUserData, account } = await createAccount(input); + // Add the codes to the account + const code = await getCode(model, firebaseUserData); + await sendEmail('emailCodeSignIn', account.language, input.email, { firstname: account.firstName || 'stranger', code }); + return { message: 'sent' }; + } catch (error) { + throw error; + } + } +} + +async function getCode(model: AccountModel, data: AccountModel) { + // Get the codes + let codes = data.authCodes; + if (codes) { + // Remove the expired ones + _.remove(codes, (e) => { + const diffInMs = DateTime.fromJSDate(e.expiresAt).diffNow('milliseconds').milliseconds; + const MAX_TIMEOUT = 15 * 60 * 1000; + + if (diffInMs >= MAX_TIMEOUT) return true; + + return false; + }); + } else codes = []; + + // Check if too many attempt in the last 15 minutes + if (codes.length > 5) throw newError(2302); + + // Add the new one + const code = generateCodeNumber(); + + codes.push({ + code: code, + expiresAt: DateTime.utc() + .plus({ minutes: 15 }) + .toJSDate(), + }); + // Save in DB + await model.updateOneCustom( + { _id: data._id }, + { + $set: { authCodes: codes }, + }, + + null, + ); + return code; +} + +async function getAccount(email: string) { + let firebaseUserData; + try { + firebaseUserData = await Firebase.getInstance().getUserByEmail(email); + } catch (error) { + throw newError(2004, error); + } + + // Check if in DB or not + const model = new AccountModel(); + + const accountCheck = await (await model.db()).findOne({ _id: firebaseUserData.uid }); + // Verify if has thinkific signin + if (!accountCheck) throw newError(2004); + return { accountCheck, model, firebaseUserData }; +} + +async function createAccount(input: CodeRequestInput) { + let firebaseUserData; + try { + firebaseUserData = await Firebase.getInstance().getUserByEmail(input.email); + } catch (error) { + try { + firebaseUserData = await Firebase.getInstance().createUser({ + email: input.email, + password: uuid(), + }); + } catch (error) { + throw error; + } + } + + const dataToSave = { + ...input, + firebaseData: { + aud: firebaseUserData.aud, + emailVerified: firebaseUserData.email_verified, + firebase: JSON.stringify(firebaseUserData.firebase), + }, + updatedAt: new Date(), + }; + + // Check if in DB or not + const model = new AccountModel(); + const type = AccountTypeEnum.user; + + let account; + + const accountCheck = await (await model.db()).findOne({ _id: firebaseUserData.uid }); + // Verify if has thinkific signin + if (accountCheck) { + account = accountCheck; + await (await model.db()).updateOne( + { _id: firebaseUserData.uid }, + { + $set: { ...input }, + }, + ); + } else { + const types = [type]; + account = await model.saveOne( + { + ...dataToSave, + _id: firebaseUserData.uid, + types, + r: [AccountTypeEnum.admin, firebaseUserData.uid], + w: [AccountTypeEnum.admin, firebaseUserData.uid], + d: [AccountTypeEnum.admin, firebaseUserData.uid], + }, + null, + ); + } + return { model, firebaseUserData, account }; +} diff --git a/services/module-accounts/services/AccountService.ts b/services/module-accounts/services/AccountService.ts new file mode 100644 index 0000000..1c1deed --- /dev/null +++ b/services/module-accounts/services/AccountService.ts @@ -0,0 +1,72 @@ +import Firebase from '@seed/services/auth/FirebaseService'; +import { v4 as uuid } from 'uuid'; + +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { checkPermissions } from '@seed/graphql/AccessService'; +import { ApolloContext } from '@seed/interfaces/context'; +import { saveAccountToDB, saveAccountToDBAnonymous } from './account.service.helper'; +import FirebaseAuth from '@seed/services/auth/FirebaseAuthService'; +import { newError } from '@seed/helpers/Error'; + +export const signinGeneric = async (model: any, type: AccountTypeEnum, body: any, ctx: ApolloContext): Promise => { + try { + if (body.passwordConfirmation != body.password) throw newError(2006); + + const result = await saveAccountToDB(model, type, body, ctx); + + return result; + } catch (error) { + throw error; + } +}; + +export const signinGenericNoPassword = async (model: any, type: AccountTypeEnum, body: any, ctx: ApolloContext): Promise => { + try { + body.password = uuid(); + const result = await saveAccountToDB(model, type, body, ctx); + + return result; + } catch (error) { + throw error; + } +}; + +export const signinAnonymous = async (ctx: ApolloContext): Promise => { + try { + // Create the user in Firebase + const firebaseData = await FirebaseAuth.getInstance().loginAnonymous(); + // Save him in our database + await saveAccountToDBAnonymous(firebaseData, ctx); + + return firebaseData; + } catch (error) { + throw error; + } +}; + +export const deleteOneGeneric = async (model: any, type: AccountTypeEnum, id: string, ctx: ApolloContext): Promise => { + try { + const account = ctx.ctx.user; + + await model.getOne({ _id: id }, ctx); + checkPermissions(model, account, 'd'); + + // let firebaseUserData; + // Check if exists on firebase already + await Firebase.getInstance().deleteUser({ uid: id }); + + // // user exists + // if (firebaseUserGetResult.success) { + // firebaseUserData = firebaseUserGetResult.data; + // } else { + // throw firebaseUserGetResult.message; + // } + + const accountDataR = await (await model.db()).findOneAndDelete({ _id: id }); + if (accountDataR.ok == 0) throw newError(2007, accountDataR); + + return accountDataR.value; + } catch (error) { + throw error; + } +}; diff --git a/services/module-accounts/services/account.service.helper.ts b/services/module-accounts/services/account.service.helper.ts new file mode 100644 index 0000000..0cef7bb --- /dev/null +++ b/services/module-accounts/services/account.service.helper.ts @@ -0,0 +1,136 @@ +import { newError } from '@seed/helpers/Error'; +import { FirebaseTokenResult } from '@seed/interfaces/components'; +import { ApolloContext } from '@seed/interfaces/context'; +import Firebase from '@seed/services/auth/FirebaseService'; +import { createSetRequest } from '@seed/services/database/DBService'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import AccountModel from '@src/accounts/account.model'; +import { ApolloError } from 'apollo-server-lambda'; + +import { v4 as uuid } from 'uuid'; +import BaseAccountModel from '../account.model'; + +export const saveAccountToDB = async (model: T, type: AccountTypeEnum, body: any, ctx: ApolloContext): Promise => { + try { + const organisationId = ctx.ctx.organisationId; + + // Check if exists on firebase already + let firebaseUserData; + try { + firebaseUserData = await Firebase.getInstance().getUserByEmail(body.email); + } catch (error) { + try { + firebaseUserData = await Firebase.getInstance().createUser({ + email: body.email, + password: body.password, + }); + } catch (error) { + throw error; + } + } + + // Transform + delete body.password; + delete body.passwordConfirmation; + delete body.type; + + const dataToSave = { + ...body, + firebaseData: { + aud: firebaseUserData.aud, + emailVerified: firebaseUserData.email_verified, + firebase: JSON.stringify(firebaseUserData.firebase), + }, + updatedAt: new Date(), + }; + + // Check if in DB or not + let currentUserModel; + try { + currentUserModel = await model.getOne({ _id: firebaseUserData.uid }, null); + } catch (error) {} + + // If account exists -> Update this one + if (currentUserModel) { + // Check if already registered for that type + if (model.types.includes(type)) throw newError(2003); + + model.set(dataToSave); + model.types.push(type); + + if (organisationId) { + if (model.organisationIds) model.organisationIds.push(organisationId); + else model.organisationIds = [organisationId]; + } + + model.types = [...new Set(model.types)]; + model.organisationIds = [...new Set(model.organisationIds)]; + + return await model.updateOne( + { _id: model._id as any }, + { + ...model.get(), + }, + null, + ); + } + // If account doesn't exists -> Create this one + else { + const types = [type]; + if (organisationId) dataToSave.organisationIds = [organisationId]; + return await model.saveOne( + { + ...dataToSave, + _id: firebaseUserData.uid, + types, + r: [AccountTypeEnum.admin, firebaseUserData.uid], + w: [AccountTypeEnum.admin, firebaseUserData.uid], + d: [AccountTypeEnum.admin, firebaseUserData.uid], + }, + null, + ); + } + } catch (error) { + throw error; + } +}; + +export const saveAccountToDBAnonymous = async (tokenResult: FirebaseTokenResult, ctx: ApolloContext): Promise => { + try { + const model = new AccountModel(); + + const organisationId = ctx.ctx.organisationId; + + const addToSet: any = { + types: AccountTypeEnum.public, + }; + + if (organisationId) addToSet.organisationIds = organisationId; + + const dataToSave = { + email: 'guest-' + uuid() + '@firebase.com', + updatedAt: new Date(), + }; + + const accountDataR = await (await model.db()).findOneAndUpdate( + { _id: tokenResult.localId }, + { + $set: createSetRequest('', dataToSave), + $addToSet: addToSet, + $setOnInsert: { + _id: tokenResult.localId, + r: [AccountTypeEnum.admin, tokenResult.localId], + w: [AccountTypeEnum.admin, tokenResult.localId], + d: [AccountTypeEnum.admin, tokenResult.localId], + createdAt: new Date(), + }, + }, + { upsert: true, returnOriginal: false }, + ); + if (accountDataR.ok == 0) throw newError(2005, accountDataR); + + return accountDataR.value; + } catch (error) { + throw newError(2005, error); + } +}; diff --git a/services/module-booking/.gitignore b/services/module-booking/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/services/module-booking/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/services/module-booking/README.md b/services/module-booking/README.md new file mode 100644 index 0000000..47a4941 --- /dev/null +++ b/services/module-booking/README.md @@ -0,0 +1,23 @@ +# module-booking + +## CONFIGURATION + +- Add the component in the project components +```` +export enum BookingsRessourceEnum { + workspaces = 'workspaces', +} +registerEnumType(BookingsRessourceEnum, { + name: 'BookingsRessourceEnum', +}); + +```` + +- Register resolver for the different ressources. In src create a bookings folder with a bookingFieldResolver + +- Import BookingEngineResolver and the field Revolver of your project + + + +## TODO +-- On edit verify ownership \ No newline at end of file diff --git a/services/module-booking/__tests/playground.gql b/services/module-booking/__tests/playground.gql new file mode 100644 index 0000000..536fc30 --- /dev/null +++ b/services/module-booking/__tests/playground.gql @@ -0,0 +1,11 @@ +mutation createBooking { + createOneBooking(input:{ressourceModel:workspaces,ressourceId:"718aa94e-f13d-4ee3-9610-c12c98750c08",dates:{startDate:"2020-11-01T23:15:17.044+0000",endDate:"2020-11-02T23:15:17.044+0000"},capacity:10}){_id,ownerId,owner{email}} +} + +query bookingsGetMany { + bookingsGetMany{_id} +} + +query bookingsGetOne { + bookingsGetOne(id:"2a3c6b3d-e8e5-4a4d-9c10-6e438ac22e10"){_id} +} \ No newline at end of file diff --git a/services/module-booking/components.errors.ts b/services/module-booking/components.errors.ts new file mode 100644 index 0000000..5b9a486 --- /dev/null +++ b/services/module-booking/components.errors.ts @@ -0,0 +1,6 @@ +export const engineBookingErrorConfig = { + /* Bookings error */ + 7000: 'That slot has already been reserved', + 7001: 'There was an error manipulating the dates', + 7002: 'That slot is not available ', +}; diff --git a/services/module-booking/functions/bookings/booking.service.ts b/services/module-booking/functions/bookings/booking.service.ts new file mode 100644 index 0000000..f70743d --- /dev/null +++ b/services/module-booking/functions/bookings/booking.service.ts @@ -0,0 +1,156 @@ +import { EngineModel } from '@seed/engine/EngineModel'; +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { newError } from '@seed/helpers/Error'; +import { converDateRangeToDateTime, isTimeSlotInDates, slotToDatesRange } from '@seed/helpers/Utils.dates'; +import { checkIfDatesAreInAvailabilities } from '@seed/helpers/Utils.dates.intervals'; +import { StatusEnum } from '@seed/interfaces/components'; +import { AvailabilityComponent, DateRangeComponent, SlotComponent } from '@seed/interfaces/components.dates'; +import { ApolloContext } from '@seed/interfaces/context'; +import { baseSearchFunction } from '@seed/services/database/DBRequestService'; +import { CheckoutEngineInput } from '@services/module-payments/functions/orders/schemas/order.schema.input'; +import { BookingArgsSchema, BookingNewInputSchema } from '@src/bookings/schemas/bookings.input'; +import { BookingSchema, BookingDBInterfaceSchema } from '@src/bookings/schemas/bookings.schema'; +import { BookingsRessourceEnum } from '@src/__components/components'; +import _ from 'lodash'; +import { concat } from 'lodash'; +import { DateTime } from 'luxon'; +import { DurationTypeEnum } from '../components'; +import BookingModel from './bookings.engine.model'; + +export interface NewOneBookingInterface { + model: T; + input: BookingNewInputSchema; + status: StatusEnum; + availabilities?: AvailabilityComponent; + ctx: ApolloContext; +} + +export interface SaveOneBookingInterface extends NewOneBookingInterface { + ressourceModel: BookingsRessourceEnum; + ressourceId: string; + rest: { + capacity: number; + comments?: string | undefined; + checkoutInfo?: CheckoutEngineInput | undefined; + durationType: DurationTypeEnum; + startToEnd?: DateRangeComponent | undefined; + slot?: SlotComponent | undefined; + }; + permissions: any; +} + +export class BookingService { + async getBookings(args: BookingArgsSchema, ctx?: ApolloContext | null): Promise { + const insi: any = { + model: new BookingModel() as any, + engine: true, + query: args, + ctx: ctx, + }; + + if (args.pagination) insi.pagination = args.pagination; + else insi.all = true; + + return baseSearchFunction(insi); + } + + async getBookingsToFilter(ressourceType: BookingsRessourceEnum, args: SlotComponent): Promise { + const idsToOmit: string[] = []; + + const bookings = await this.getBookings({ + dateRange: { + startDate: DateTime.fromJSDate(args.startDate).startOf('day').toJSDate(), + endDate: DateTime.fromJSDate(args.endDate).endOf('day').toJSDate(), + }, + }); + + if (bookings && bookings.length > 0) { + // Get the booking that are in those specifiq hours + bookings.forEach((element) => { + const bookingDates = converDateRangeToDateTime(element.dates); + if (isTimeSlotInDates(bookingDates, args.startTime || '00:00', args.endTime || '23:59')) { + const pIndex = _.findIndex(element.paths, { ressourceModel: ressourceType }); + if (pIndex !== -1) idsToOmit.push(element.paths[pIndex].ressourceId); + } + }); + } + + return _.uniq(idsToOmit); + } + + async createOneBookingWithEngine>(data: NewOneBookingInterface): Promise { + const { model, status, ctx, input, availabilities } = data; + const { ressourceModel, ressourceId, ...rest } = input; + + const permissions: any = { + r: [ctx.ctx.user._id, ...model.permissions.r], + w: [ctx.ctx.user._id, ...model.permissions.w], + d: [ctx.ctx.user._id, ...model.permissions.d], + }; + + return await this.saveBooking({ input, ressourceModel, ressourceId, rest, status, model, ctx, permissions, availabilities }); + } + + async createOneBookingWithModel(data: NewOneBookingInterface): Promise { + const { model, status, ctx, input, availabilities } = data; + const { ressourceModel, ressourceId, ...rest } = input; + + _.remove(model.r, function(n) { return n == "public";}) + _.remove(model.w, function(n) { return n == "public";}) + _.remove(model.d, function(n) { return n == "public";}) + + const permissions: any = { + r: [ctx.ctx.user._id, ...model.r], + w: [ctx.ctx.user._id, ...model.w], + d: [ctx.ctx.user._id, ...model.d], + }; + + + + return await this.saveBooking({ input, ressourceModel, ressourceId, rest, status, model, ctx, permissions, availabilities }); + } + + async saveBooking | BaseGraphModel>(data: SaveOneBookingInterface) { + const { ressourceModel, ressourceId, rest, status, model, ctx, permissions, input, availabilities } = data; + + const zone = process.env.TIMEZONE || 'utc'; + + const dates: DateRangeComponent[] = []; + let ressourceIdsToOmit: string[] = []; + + // (1) First check, in the bookings already made + + if (input.durationType == DurationTypeEnum.startToEnd && input.startToEnd) dates.push(input.startToEnd); + else if (input.durationType == DurationTypeEnum.slot && input.slot) dates.push(...slotToDatesRange(input.slot)); + + if (dates.length == 0) throw newError(7001); + + // (1) First check, in the availabilities + if (availabilities) { + const inAvailability = checkIfDatesAreInAvailabilities(converDateRangeToDateTime(dates, { zone }), availabilities); + if (!inAvailability) throw newError(7002); + } + + // (2) Second check + // Verify if no overlaps with others + if (input.slot) ressourceIdsToOmit = await this.getBookingsToFilter(ressourceModel, input.slot); + // Check if overlap + if (ressourceIdsToOmit.includes(ressourceId)) throw newError(7000); + + const newData: BookingDBInterfaceSchema = { + ...rest, + status, + dates, + paths: concat((model as any).getPath()), + ownerId: ctx.ctx.user._id, + }; + + return await new BookingModel(newData).saveOne({ + additionnalData: { + paths: concat((model as any).getPath()), + permissions, + }, + ctx, + }); + } +} diff --git a/services/module-booking/functions/bookings/bookings.engine.model.ts b/services/module-booking/functions/bookings/bookings.engine.model.ts new file mode 100644 index 0000000..0742fa0 --- /dev/null +++ b/services/module-booking/functions/bookings/bookings.engine.model.ts @@ -0,0 +1,71 @@ +import { ObjectType } from 'type-graphql'; +import { plainToClass } from 'class-transformer'; + +import { EngineModel } from '@lib/seed/engine/EngineModel'; +import { Permission } from '@seed/interfaces/permission'; +import { CustomSearchEngine } from '@seed/services/database/DBRequestService'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { BookingDBInterfaceSchema, BookingDBSchema, BookingSchema } from '@src/bookings/schemas/bookings.schema'; +import { BookingArgsSchema } from '@src/bookings/schemas/bookings.input'; +import { RangeType } from '@seed/interfaces/components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class BookingModel extends EngineModel { + public constructor(input?: BookingDBInterfaceSchema & Partial) { + const dataInit = plainToClass(BookingDBSchema, input || {}); + super({ + // ...init, + collectionName: ModelCollectionEnum.bookings, + permissions: permissions, + dataInit, + }); + } + + plainToClass(plain: any): BookingSchema | BookingSchema[] { + return plainToClass(BookingSchema, plain); + } + + searchEngine(): CustomSearchEngine { + const sEngine: CustomSearchEngine = { + dateTab: { + operation: 'dateTab', + dateTabConfig: { + dbFromField: 'dates.startDate', + dbToField: 'dates.endDate', + }, + }, + dateRange: { + operation: 'dateRange', + dateRangeConfig: { + inputFromField: 'startDate', + inputToField: 'endDate', + dbFromField: 'dates.startDate', + dbToField: 'dates.endDate', + dateRangeType: RangeType.intersect, + }, + }, + ressourceId: { + operation: '$eq', + dbFName: 'paths.ressourceId', + }, + ressourceModel: { + operation: '$eq', + dbFName: 'paths.ressourceModel', + }, + status: { + operation: '$in', + }, + }; + + return sEngine; + } +} diff --git a/services/module-booking/functions/bookings/resolvers/bookings.engine.resolver.ts b/services/module-booking/functions/bookings/resolvers/bookings.engine.resolver.ts new file mode 100644 index 0000000..a6ebf4f --- /dev/null +++ b/services/module-booking/functions/bookings/resolvers/bookings.engine.resolver.ts @@ -0,0 +1,42 @@ +import { Resolver, Ctx, Mutation, Arg, Authorized } from 'type-graphql'; + +import BookingModel from '../bookings.engine.model'; +import { createEngineQueryResolver } from '@seed/engine/genericResolvers/BaseEngineResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { BookingEngineArgsSchema, BookingEngineNewInputSchema } from '../schemas/bookings.engine.input'; +import { BookingEngineSchema } from '../schemas/bookings.engine.schema'; + +const BookingGenericQueryResolver = createEngineQueryResolver({ + domain: 'bookings', + schemaName: BookingEngineSchema, + modelName: BookingModel, + argsType: BookingEngineArgsSchema, + engineMiddleware: { + authorization: [AccountTypeEnum.user], + }, +}); + +@Resolver(BookingEngineSchema) +export class BookingEngineQueryResolver extends BookingGenericQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ +} + +export class BookingEngineMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} + +export const BookingEngineResolvers = [BookingEngineQueryResolver, BookingEngineMutationResolver]; diff --git a/services/module-booking/functions/bookings/schemas/bookings.engine.input.ts b/services/module-booking/functions/bookings/schemas/bookings.engine.input.ts new file mode 100644 index 0000000..acf2d87 --- /dev/null +++ b/services/module-booking/functions/bookings/schemas/bookings.engine.input.ts @@ -0,0 +1,43 @@ +import { Field, InputType, Int, ArgsType, ObjectType } from 'type-graphql'; + +import { GetManyArgsWithDateRange } from '@seed/graphql/Request'; +import { BookingEngineBaseSchema } from './bookings.engine.schema'; +import { IsRefExist } from '@seed/engine/decorators/db.guard'; +import { BookingsRessourceEnum } from '@src/__components/components'; +import { IsNotEmpty, ValidateIf, ValidateNested } from 'class-validator'; + +import { StatusEnum } from '@seed/interfaces/components'; + +@ObjectType() +@InputType() +export class BookingEngineNewInputSchema extends BookingEngineBaseSchema { + @Field(() => BookingsRessourceEnum) + @IsNotEmpty() + ressourceModel: BookingsRessourceEnum; + + @Field() + @IsNotEmpty() + @IsRefExist() + ressourceId: string; +} + +@InputType() +export class BookingEngineEditInputSchema { + @Field(() => Int, { nullable: true }) + capacity: number; + + @Field({ nullable: true }) + comments?: string; +} + +@ArgsType() +export class BookingEngineArgsSchema extends GetManyArgsWithDateRange { + @Field(() => BookingsRessourceEnum, { nullable: true }) + ressourceModel?: BookingsRessourceEnum; + + @Field({ nullable: true }) + ressourceId?: string; + + @Field(() => [StatusEnum], { nullable: true }) + status?: StatusEnum[]; +} diff --git a/services/module-booking/functions/bookings/schemas/bookings.engine.schema.ts b/services/module-booking/functions/bookings/schemas/bookings.engine.schema.ts new file mode 100644 index 0000000..09f1752 --- /dev/null +++ b/services/module-booking/functions/bookings/schemas/bookings.engine.schema.ts @@ -0,0 +1,76 @@ +import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema'; +import { EnginePathComponent, StatusEnum } from '@seed/interfaces/components'; +import { DateRangeComponent, SlotComponent } from '@seed/interfaces/components.dates'; +import { ApolloContext } from '@seed/interfaces/context'; +import AccountModel from '@src/accounts/account.model'; +import { Type } from 'class-transformer'; +import { ValidateNested, IsNotEmpty, ValidateIf } from 'class-validator'; +import { Ctx, Field, InputType, Int, ObjectType } from 'type-graphql'; +import { DurationTypeEnum } from '../../components'; + +@ObjectType() +@InputType() +export class BookingEngineBaseSchema { + @Field(() => DurationTypeEnum) + @IsNotEmpty() + durationType: DurationTypeEnum; + + @Field(() => DateRangeComponent, { nullable: true }) + @Type(() => DateRangeComponent) + @ValidateIf((o: BookingEngineBaseSchema) => { + return o.durationType == DurationTypeEnum.startToEnd; + }) + @IsNotEmpty() + @ValidateNested() + startToEnd?: DateRangeComponent; + + @Field(() => SlotComponent, { nullable: true }) + @Type(() => SlotComponent) + @ValidateIf((o: BookingEngineBaseSchema) => { + return o.durationType == DurationTypeEnum.slot; + }) + @IsNotEmpty() + @ValidateNested() + slot?: SlotComponent; + + @Field(() => Int, { nullable: true }) + capacity?: number; + + @Field({ nullable: true }) + comments?: string; +} + +@ObjectType() +export class BookingEngineDBInterfaceSchema extends BookingEngineBaseSchema { + @Field(() => StatusEnum) + status: StatusEnum; + + @Field(() => [EnginePathComponent]) + paths: EnginePathComponent[]; + + @Field() + @IsNotEmpty() + ownerId: string; + + @Field(() => [DateRangeComponent]) + @IsNotEmpty() + dates: DateRangeComponent[]; +} + +@ObjectType() +export class BookingEngineDBSchema extends BookingEngineDBInterfaceSchema { + @Field(() => AccountModel) + async owner(@Ctx() ctx: ApolloContext): Promise { + return ctx.ctx.loaders.accountLoader.load(this.ownerId); + } +} + +@ObjectType({ implements: IEngineSchema }) +export class BookingEngineSchema extends BookingEngineDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/services/module-booking/functions/components.ts b/services/module-booking/functions/components.ts new file mode 100644 index 0000000..c4d648e --- /dev/null +++ b/services/module-booking/functions/components.ts @@ -0,0 +1,44 @@ +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +import { IsRefExist } from '@seed/engine/decorators/db.guard'; +import { BookingsRessourceEnum } from '@src/__components/components'; +import { IsNotEmpty } from 'class-validator'; +import { Field, ObjectType, registerEnumType } from 'type-graphql'; + +export enum DurationTypeEnum { + startToEnd = 'startToEnd', + slot = 'slot', +} +registerEnumType(DurationTypeEnum, { + name: 'DurationTypeEnum', +}); + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@ObjectType() +export class GenericRefComponent { + @Field(() => BookingsRessourceEnum) + @IsNotEmpty() + ressourceModel: BookingsRessourceEnum; + + @Field() + @IsNotEmpty() + @IsRefExist() + ressourceId: string; +} diff --git a/services/module-booking/functions/crons/booking.cron.service.ts b/services/module-booking/functions/crons/booking.cron.service.ts new file mode 100644 index 0000000..a32fc02 --- /dev/null +++ b/services/module-booking/functions/crons/booking.cron.service.ts @@ -0,0 +1,31 @@ +import { StatusEnum } from "@seed/interfaces/components"; +import { SuccessResponse } from "@seed/interfaces/response"; +import DB from "@seed/services/database/DBService"; +import _ from "lodash"; +import { DateTime } from "luxon"; +import BookingModel from "../bookings/bookings.engine.model"; + +export class BookingCronService { + async removeDrafts(): Promise { + const bookings = await new BookingModel().getAll({ query:{ + 'createdAt' : {"$lte": DateTime.utc().minus({minutes:30}).toJSDate()}, + 'status': StatusEnum.draft, + }}) + + bookings.forEach((element,index) => { + bookings[index].status = StatusEnum.expired; + }); + + if (bookings.length > 0) { + // Move them to the other collection + await (await DB.getInstance()).db.collection('bookings.archives').insertMany(bookings); + + const bookingIds = _.map(bookings,'_id'); + await (await new BookingModel().db()).deleteMany({_id:{'$in':bookingIds}}); + + } + + return {success:true}; + } + +} diff --git a/services/module-booking/functions/helper.ts b/services/module-booking/functions/helper.ts new file mode 100644 index 0000000..55922dc --- /dev/null +++ b/services/module-booking/functions/helper.ts @@ -0,0 +1,63 @@ +import { getMinutesFromTimeInString, getNumberOfWeekendDays } from '@seed/helpers/Utils.dates'; +import { DateRangeComponent, SlotComponent } from '@seed/interfaces/components.dates'; +import { DateTime } from 'luxon'; + +export interface StartToEndFormat { + bookingDate: string; + bookingStart: string; + bookingEnd: string; + bookingDurationInMinutes: number; +} + +export interface SlotFormat { + bookingStart: string; + bookingEnd: string; + bookingHourStart: string; + bookingHourEnd: string; + bookingDurationInMinutes: number; +} + +export const formatStartToEnd = (input: DateRangeComponent, options?: { zone: string }): StartToEndFormat => { + const zone = options?.zone || process.env.TIMEZONE || 'utc'; + + const bookingStart = DateTime.fromJSDate(input.startDate, { zone }); + const bookingEnd = DateTime.fromJSDate(input.endDate, { zone }); + const durationObject = bookingEnd.diff(bookingStart, 'minutes').toObject(); + + return { + bookingDate: bookingStart.toFormat('dd/MM/yyyy'), + bookingStart: bookingStart.toFormat('HH:mm'), + bookingEnd: bookingEnd.toFormat('HH:mm'), + bookingDurationInMinutes: durationObject.minutes || 0, + }; +}; + +export const formatSlot = (input: SlotComponent, options?: { zone: string; removeWeekend?: boolean }): SlotFormat => { + const zone = options?.zone || process.env.TIMEZONE || 'utc'; + const removeWeekend = options?.removeWeekend || false; + const { startDate, endDate, startTime, endTime } = input; + + const bookingStart = DateTime.fromJSDate(startDate, { zone }); + let bookingEnd = DateTime.fromJSDate(endDate, { zone }); + const numberOfDay = bookingEnd.diff(bookingStart, 'days').toObject(); + + const startTimeInMin = getMinutesFromTimeInString(startTime); + const endTimeInMin = getMinutesFromTimeInString(endTime); + + let daysToRemove = 0; + if (removeWeekend) { + daysToRemove = getNumberOfWeekendDays(bookingStart, bookingEnd); + } + + const finalNumberOfDays = (numberOfDay.day || numberOfDay.days || 0) + 1 - daysToRemove; + + bookingEnd = bookingEnd.minus({ second: 1 }); + + return { + bookingStart: bookingStart.toFormat('dd/MM/yyyy'), + bookingEnd: bookingEnd.toFormat('dd/MM/yyyy'), + bookingHourStart: startTime, + bookingHourEnd: endTime, + bookingDurationInMinutes: finalNumberOfDays * (endTimeInMin - startTimeInMin), + }; +}; diff --git a/services/module-cms/.gitignore b/services/module-cms/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/services/module-cms/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/services/module-cms/README.md b/services/module-cms/README.md new file mode 100644 index 0000000..710a2c8 --- /dev/null +++ b/services/module-cms/README.md @@ -0,0 +1,7 @@ +# Make-it CMS Service + +[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com) + +![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) + +### This is a private repo diff --git a/services/module-cms/__tests/pages.playground.graphql b/services/module-cms/__tests/pages.playground.graphql new file mode 100644 index 0000000..bce4c15 --- /dev/null +++ b/services/module-cms/__tests/pages.playground.graphql @@ -0,0 +1,31 @@ +query pages { + pages { + _id + title + content + } +} + +mutation addOnePage { + pagesAddOne(input: { title: "test New Title" }) { + _id + title + content + } +} + +mutation editOnePage { + pagesEditOne(id: "b59b36db-7f2c-44ab-8793-b47f773a2d2e", input: { title: "Updated Title" }) { + _id + title + content + } +} + +mutation deleteOnePage { + pagesDeleteOne(id: "b59b36db-7f2c-44ab-8793-b47f773a2d2e") { + _id + title + content + } +} diff --git a/services/module-cms/__tests/project-settings.playground.graphql b/services/module-cms/__tests/project-settings.playground.graphql new file mode 100644 index 0000000..54c8d73 --- /dev/null +++ b/services/module-cms/__tests/project-settings.playground.graphql @@ -0,0 +1 @@ +mutation projectSettingsAddOne {projectSettingsAddOne(input:{key:"FILE_allowedMime",value:"image/jpeg,image/png,image/gif,application/pdf,image/svg+xml,application/xml,application/zip"}){_id}} \ No newline at end of file diff --git a/services/module-cms/components.ts b/services/module-cms/components.ts new file mode 100644 index 0000000..7dfde2a --- /dev/null +++ b/services/module-cms/components.ts @@ -0,0 +1,33 @@ +import { registerEnumType, ObjectType, Field, InputType } from 'type-graphql'; +import { TranslatableComponent } from '@src/__components/components'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@ObjectType() +@InputType('SEOEventInput') +export class SEOEvent { + @Field() + title: TranslatableComponent; + @Field() + description: TranslatableComponent; + @Field() + keywords: TranslatableComponent; +} diff --git a/services/module-cms/functions/categories/category.model.ts b/services/module-cms/functions/categories/category.model.ts new file mode 100644 index 0000000..62a6cd5 --- /dev/null +++ b/services/module-cms/functions/categories/category.model.ts @@ -0,0 +1,80 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { GetArgs } from '@seed/graphql/Request'; + +import { TranslatableComponent, RessourceEnum } from '@src/__components/components'; + +import { Permission } from '@seed/interfaces/permission'; +import BaseSEOModel from '@seed/graphql/baseModels/BaseSeoModel'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class CategoryModel extends BaseSEOModel { + public constructor() { + super('categories', permissions); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field(() => RessourceEnum) + ressourceType: RessourceEnum; + + @Field(() => TranslatableComponent) + title: TranslatableComponent; + + @Field(() => TranslatableComponent, { nullable: true }) + content: TranslatableComponent; + + @Field({ nullable: true }) + colorCode?: string; + + searchOptions(): string[] { + return ['title']; + } +} + +@ArgsType() +export class CategoryArgs { + @Field({ nullable: true }) + search?: string; + + @Field(() => RessourceEnum) + ressourceType: RessourceEnum; + + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@InputType() +export class NewCategoryInput implements Partial { + @Field(() => RessourceEnum) + ressourceType: RessourceEnum; + + @Field(() => TranslatableComponent) + title: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + content: TranslatableComponent; + @Field({ nullable: true }) + colorCode?: string; +} + +@InputType() +export class EditCategoryInput implements Partial { + @Field(() => RessourceEnum) + ressourceType: RessourceEnum; + + @Field(() => TranslatableComponent) + title: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + content: TranslatableComponent; + @Field({ nullable: true }) + colorCode?: string; +} diff --git a/services/module-cms/functions/categories/category.resolver.admin.ts b/services/module-cms/functions/categories/category.resolver.admin.ts new file mode 100644 index 0000000..1bb00e0 --- /dev/null +++ b/services/module-cms/functions/categories/category.resolver.admin.ts @@ -0,0 +1,28 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import CategoryModel, { CategoryArgs, NewCategoryInput, EditCategoryInput } from '@services/module-cms/functions/categories/category.model'; + +const CategoryAdminBaseResolver = createBaseResolver('categories', CategoryModel, CategoryArgs, NewCategoryInput, EditCategoryInput, [ + AccountTypeEnum.admin, +]); + +@Resolver(CategoryModel) +export default class CategoryAdminResolver extends CategoryAdminBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-cms/functions/categories/category.resolver.fields.ts b/services/module-cms/functions/categories/category.resolver.fields.ts new file mode 100644 index 0000000..05380ab --- /dev/null +++ b/services/module-cms/functions/categories/category.resolver.fields.ts @@ -0,0 +1,15 @@ +import { Resolver } from 'type-graphql'; + +import CategoryModel from '@services/module-cms/functions/categories/category.model'; + +@Resolver(CategoryModel) +export default class CategoryFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-cms/functions/categories/category.resolver.ts b/services/module-cms/functions/categories/category.resolver.ts new file mode 100644 index 0000000..65bcff6 --- /dev/null +++ b/services/module-cms/functions/categories/category.resolver.ts @@ -0,0 +1,26 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; + +import CategoryModel, { CategoryArgs } from '@services/module-cms/functions/categories/category.model'; +import { createBasePublicResolver } from '@seed/graphql/baseResolvers/BasePublicResolver'; + +const CategoryBaseResolver = createBasePublicResolver('categories', CategoryModel, CategoryArgs); + +@Resolver(CategoryModel) +export default class CategoryResolver extends CategoryBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-cms/functions/countries/countries.json b/services/module-cms/functions/countries/countries.json new file mode 100644 index 0000000..a25c983 --- /dev/null +++ b/services/module-cms/functions/countries/countries.json @@ -0,0 +1,1254 @@ +{ + "countries": [ + { + "countryCode": "AD", + "countryName": "Andorra", + "currencyCode": "EUR" + }, + { + "countryCode": "AE", + "countryName": "United Arab Emirates", + "currencyCode": "AED" + }, + { + "countryCode": "AF", + "countryName": "Afghanistan", + "currencyCode": "AFN" + }, + { + "countryCode": "AG", + "countryName": "Antigua and Barbuda", + "currencyCode": "XCD" + }, + { + "countryCode": "AI", + "countryName": "Anguilla", + "currencyCode": "XCD" + }, + { + "countryCode": "AL", + "countryName": "Albania", + "currencyCode": "ALL" + }, + { + "countryCode": "AM", + "countryName": "Armenia", + "currencyCode": "AMD" + }, + { + "countryCode": "AO", + "countryName": "Angola", + "currencyCode": "AOA" + }, + { + "countryCode": "AQ", + "countryName": "Antarctica", + "currencyCode": "" + }, + { + "countryCode": "AR", + "countryName": "Argentina", + "currencyCode": "ARS" + }, + { + "countryCode": "AS", + "countryName": "American Samoa", + "currencyCode": "USD" + }, + { + "countryCode": "AT", + "countryName": "Austria", + "currencyCode": "EUR" + }, + { + "countryCode": "AU", + "countryName": "Australia", + "currencyCode": "AUD" + }, + { + "countryCode": "AW", + "countryName": "Aruba", + "currencyCode": "AWG" + }, + { + "countryCode": "AX", + "countryName": "Åland", + "currencyCode": "EUR" + }, + { + "countryCode": "AZ", + "countryName": "Azerbaijan", + "currencyCode": "AZN" + }, + { + "countryCode": "BA", + "countryName": "Bosnia and Herzegovina", + "currencyCode": "BAM" + }, + { + "countryCode": "BB", + "countryName": "Barbados", + "currencyCode": "BBD" + }, + { + "countryCode": "BD", + "countryName": "Bangladesh", + "currencyCode": "BDT" + }, + { + "countryCode": "BE", + "countryName": "Belgium", + "currencyCode": "EUR" + }, + { + "countryCode": "BF", + "countryName": "Burkina Faso", + "currencyCode": "XOF" + }, + { + "countryCode": "BG", + "countryName": "Bulgaria", + "currencyCode": "BGN" + }, + { + "countryCode": "BH", + "countryName": "Bahrain", + "currencyCode": "BHD" + }, + { + "countryCode": "BI", + "countryName": "Burundi", + "currencyCode": "BIF" + }, + { + "countryCode": "BJ", + "countryName": "Benin", + "currencyCode": "XOF" + }, + { + "countryCode": "BL", + "countryName": "Saint Barthélemy", + "currencyCode": "EUR" + }, + { + "countryCode": "BM", + "countryName": "Bermuda", + "currencyCode": "BMD" + }, + { + "countryCode": "BN", + "countryName": "Brunei", + "currencyCode": "BND" + }, + { + "countryCode": "BO", + "countryName": "Bolivia", + "currencyCode": "BOB" + }, + { + "countryCode": "BQ", + "countryName": "Bonaire", + "currencyCode": "USD" + }, + { + "countryCode": "BR", + "countryName": "Brazil", + "currencyCode": "BRL" + }, + { + "countryCode": "BS", + "countryName": "Bahamas", + "currencyCode": "BSD" + }, + { + "countryCode": "BT", + "countryName": "Bhutan", + "currencyCode": "BTN" + }, + { + "countryCode": "BV", + "countryName": "Bouvet Island", + "currencyCode": "NOK" + }, + { + "countryCode": "BW", + "countryName": "Botswana", + "currencyCode": "BWP" + }, + { + "countryCode": "BY", + "countryName": "Belarus", + "currencyCode": "BYR" + }, + { + "countryCode": "BZ", + "countryName": "Belize", + "currencyCode": "BZD" + }, + { + "countryCode": "CA", + "countryName": "Canada", + "currencyCode": "CAD" + }, + { + "countryCode": "CC", + "countryName": "Cocos [Keeling] Islands", + "currencyCode": "AUD" + }, + { + "countryCode": "CD", + "countryName": "Democratic Republic of the Congo", + "currencyCode": "CDF" + }, + { + "countryCode": "CF", + "countryName": "Central African Republic", + "currencyCode": "XAF" + }, + { + "countryCode": "CG", + "countryName": "Republic of the Congo", + "currencyCode": "XAF" + }, + { + "countryCode": "CH", + "countryName": "Switzerland", + "currencyCode": "CHF" + }, + { + "countryCode": "CI", + "countryName": "Ivory Coast", + "currencyCode": "XOF" + }, + { + "countryCode": "CK", + "countryName": "Cook Islands", + "currencyCode": "NZD" + }, + { + "countryCode": "CL", + "countryName": "Chile", + "currencyCode": "CLP" + }, + { + "countryCode": "CM", + "countryName": "Cameroon", + "currencyCode": "XAF" + }, + { + "countryCode": "CN", + "countryName": "China", + "currencyCode": "CNY" + }, + { + "countryCode": "CO", + "countryName": "Colombia", + "currencyCode": "COP" + }, + { + "countryCode": "CR", + "countryName": "Costa Rica", + "currencyCode": "CRC" + }, + { + "countryCode": "CU", + "countryName": "Cuba", + "currencyCode": "CUP" + }, + { + "countryCode": "CV", + "countryName": "Cape Verde", + "currencyCode": "CVE" + }, + { + "countryCode": "CW", + "countryName": "Curacao", + "currencyCode": "ANG" + }, + { + "countryCode": "CX", + "countryName": "Christmas Island", + "currencyCode": "AUD" + }, + { + "countryCode": "CY", + "countryName": "Cyprus", + "currencyCode": "EUR" + }, + { + "countryCode": "CZ", + "countryName": "Czechia", + "currencyCode": "CZK" + }, + { + "countryCode": "DE", + "countryName": "Germany", + "currencyCode": "EUR" + }, + { + "countryCode": "DJ", + "countryName": "Djibouti", + "currencyCode": "DJF" + }, + { + "countryCode": "DK", + "countryName": "Denmark", + "currencyCode": "DKK" + }, + { + "countryCode": "DM", + "countryName": "Dominica", + "currencyCode": "XCD" + }, + { + "countryCode": "DO", + "countryName": "Dominican Republic", + "currencyCode": "DOP" + }, + { + "countryCode": "DZ", + "countryName": "Algeria", + "currencyCode": "DZD" + }, + { + "countryCode": "EC", + "countryName": "Ecuador", + "currencyCode": "USD" + }, + { + "countryCode": "EE", + "countryName": "Estonia", + "currencyCode": "EUR" + }, + { + "countryCode": "EG", + "countryName": "Egypt", + "currencyCode": "EGP" + }, + { + "countryCode": "EH", + "countryName": "Western Sahara", + "currencyCode": "MAD" + }, + { + "countryCode": "ER", + "countryName": "Eritrea", + "currencyCode": "ERN" + }, + { + "countryCode": "ES", + "countryName": "Spain", + "currencyCode": "EUR" + }, + { + "countryCode": "ET", + "countryName": "Ethiopia", + "currencyCode": "ETB" + }, + { + "countryCode": "FI", + "countryName": "Finland", + "currencyCode": "EUR" + }, + { + "countryCode": "FJ", + "countryName": "Fiji", + "currencyCode": "FJD" + }, + { + "countryCode": "FK", + "countryName": "Falkland Islands", + "currencyCode": "FKP" + }, + { + "countryCode": "FM", + "countryName": "Micronesia", + "currencyCode": "USD" + }, + { + "countryCode": "FO", + "countryName": "Faroe Islands", + "currencyCode": "DKK" + }, + { + "countryCode": "FR", + "countryName": "France", + "currencyCode": "EUR" + }, + { + "countryCode": "GA", + "countryName": "Gabon", + "currencyCode": "XAF" + }, + { + "countryCode": "GB", + "countryName": "United Kingdom", + "currencyCode": "GBP" + }, + { + "countryCode": "GD", + "countryName": "Grenada", + "currencyCode": "XCD" + }, + { + "countryCode": "GE", + "countryName": "Georgia", + "currencyCode": "GEL" + }, + { + "countryCode": "GF", + "countryName": "French Guiana", + "currencyCode": "EUR" + }, + { + "countryCode": "GG", + "countryName": "Guernsey", + "currencyCode": "GBP" + }, + { + "countryCode": "GH", + "countryName": "Ghana", + "currencyCode": "GHS" + }, + { + "countryCode": "GI", + "countryName": "Gibraltar", + "currencyCode": "GIP" + }, + { + "countryCode": "GL", + "countryName": "Greenland", + "currencyCode": "DKK" + }, + { + "countryCode": "GM", + "countryName": "Gambia", + "currencyCode": "GMD" + }, + { + "countryCode": "GN", + "countryName": "Guinea", + "currencyCode": "GNF" + }, + { + "countryCode": "GP", + "countryName": "Guadeloupe", + "currencyCode": "EUR" + }, + { + "countryCode": "GQ", + "countryName": "Equatorial Guinea", + "currencyCode": "XAF" + }, + { + "countryCode": "GR", + "countryName": "Greece", + "currencyCode": "EUR" + }, + { + "countryCode": "GS", + "countryName": "South Georgia and the South Sandwich Islands", + "currencyCode": "GBP" + }, + { + "countryCode": "GT", + "countryName": "Guatemala", + "currencyCode": "GTQ" + }, + { + "countryCode": "GU", + "countryName": "Guam", + "currencyCode": "USD" + }, + { + "countryCode": "GW", + "countryName": "Guinea-Bissau", + "currencyCode": "XOF" + }, + { + "countryCode": "GY", + "countryName": "Guyana", + "currencyCode": "GYD" + }, + { + "countryCode": "HK", + "countryName": "Hong Kong", + "currencyCode": "HKD" + }, + { + "countryCode": "HM", + "countryName": "Heard Island and McDonald Islands", + "currencyCode": "AUD" + }, + { + "countryCode": "HN", + "countryName": "Honduras", + "currencyCode": "HNL" + }, + { + "countryCode": "HR", + "countryName": "Croatia", + "currencyCode": "HRK" + }, + { + "countryCode": "HT", + "countryName": "Haiti", + "currencyCode": "HTG" + }, + { + "countryCode": "HU", + "countryName": "Hungary", + "currencyCode": "HUF" + }, + { + "countryCode": "ID", + "countryName": "Indonesia", + "currencyCode": "IDR" + }, + { + "countryCode": "IE", + "countryName": "Ireland", + "currencyCode": "EUR" + }, + { + "countryCode": "IL", + "countryName": "Israel", + "currencyCode": "ILS" + }, + { + "countryCode": "IM", + "countryName": "Isle of Man", + "currencyCode": "GBP" + }, + { + "countryCode": "IN", + "countryName": "India", + "currencyCode": "INR" + }, + { + "countryCode": "IO", + "countryName": "British Indian Ocean Territory", + "currencyCode": "USD" + }, + { + "countryCode": "IQ", + "countryName": "Iraq", + "currencyCode": "IQD" + }, + { + "countryCode": "IR", + "countryName": "Iran", + "currencyCode": "IRR" + }, + { + "countryCode": "IS", + "countryName": "Iceland", + "currencyCode": "ISK" + }, + { + "countryCode": "IT", + "countryName": "Italy", + "currencyCode": "EUR" + }, + { + "countryCode": "JE", + "countryName": "Jersey", + "currencyCode": "GBP" + }, + { + "countryCode": "JM", + "countryName": "Jamaica", + "currencyCode": "JMD" + }, + { + "countryCode": "JO", + "countryName": "Jordan", + "currencyCode": "JOD" + }, + { + "countryCode": "JP", + "countryName": "Japan", + "currencyCode": "JPY" + }, + { + "countryCode": "KE", + "countryName": "Kenya", + "currencyCode": "KES" + }, + { + "countryCode": "KG", + "countryName": "Kyrgyzstan", + "currencyCode": "KGS" + }, + { + "countryCode": "KH", + "countryName": "Cambodia", + "currencyCode": "KHR" + }, + { + "countryCode": "KI", + "countryName": "Kiribati", + "currencyCode": "AUD" + }, + { + "countryCode": "KM", + "countryName": "Comoros", + "currencyCode": "KMF" + }, + { + "countryCode": "KN", + "countryName": "Saint Kitts and Nevis", + "currencyCode": "XCD" + }, + { + "countryCode": "KP", + "countryName": "North Korea", + "currencyCode": "KPW" + }, + { + "countryCode": "KR", + "countryName": "South Korea", + "currencyCode": "KRW" + }, + { + "countryCode": "KW", + "countryName": "Kuwait", + "currencyCode": "KWD" + }, + { + "countryCode": "KY", + "countryName": "Cayman Islands", + "currencyCode": "KYD" + }, + { + "countryCode": "KZ", + "countryName": "Kazakhstan", + "currencyCode": "KZT" + }, + { + "countryCode": "LA", + "countryName": "Laos", + "currencyCode": "LAK" + }, + { + "countryCode": "LB", + "countryName": "Lebanon", + "currencyCode": "LBP" + }, + { + "countryCode": "LC", + "countryName": "Saint Lucia", + "currencyCode": "XCD" + }, + { + "countryCode": "LI", + "countryName": "Liechtenstein", + "currencyCode": "CHF" + }, + { + "countryCode": "LK", + "countryName": "Sri Lanka", + "currencyCode": "LKR" + }, + { + "countryCode": "LR", + "countryName": "Liberia", + "currencyCode": "LRD" + }, + { + "countryCode": "LS", + "countryName": "Lesotho", + "currencyCode": "LSL" + }, + { + "countryCode": "LT", + "countryName": "Lithuania", + "currencyCode": "EUR" + }, + { + "countryCode": "LU", + "countryName": "Luxembourg", + "currencyCode": "EUR" + }, + { + "countryCode": "LV", + "countryName": "Latvia", + "currencyCode": "EUR" + }, + { + "countryCode": "LY", + "countryName": "Libya", + "currencyCode": "LYD" + }, + { + "countryCode": "MA", + "countryName": "Morocco", + "currencyCode": "MAD" + }, + { + "countryCode": "MC", + "countryName": "Monaco", + "currencyCode": "EUR" + }, + { + "countryCode": "MD", + "countryName": "Moldova", + "currencyCode": "MDL" + }, + { + "countryCode": "ME", + "countryName": "Montenegro", + "currencyCode": "EUR" + }, + { + "countryCode": "MF", + "countryName": "Saint Martin", + "currencyCode": "EUR" + }, + { + "countryCode": "MG", + "countryName": "Madagascar", + "currencyCode": "MGA" + }, + { + "countryCode": "MH", + "countryName": "Marshall Islands", + "currencyCode": "USD" + }, + { + "countryCode": "MK", + "countryName": "Macedonia", + "currencyCode": "MKD" + }, + { + "countryCode": "ML", + "countryName": "Mali", + "currencyCode": "XOF" + }, + { + "countryCode": "MM", + "countryName": "Myanmar [Burma]", + "currencyCode": "MMK" + }, + { + "countryCode": "MN", + "countryName": "Mongolia", + "currencyCode": "MNT" + }, + { + "countryCode": "MO", + "countryName": "Macao", + "currencyCode": "MOP" + }, + { + "countryCode": "MP", + "countryName": "Northern Mariana Islands", + "currencyCode": "USD" + }, + { + "countryCode": "MQ", + "countryName": "Martinique", + "currencyCode": "EUR" + }, + { + "countryCode": "MR", + "countryName": "Mauritania", + "currencyCode": "MRO" + }, + { + "countryCode": "MS", + "countryName": "Montserrat", + "currencyCode": "XCD" + }, + { + "countryCode": "MT", + "countryName": "Malta", + "currencyCode": "EUR" + }, + { + "countryCode": "MU", + "countryName": "Mauritius", + "currencyCode": "MUR" + }, + { + "countryCode": "MV", + "countryName": "Maldives", + "currencyCode": "MVR" + }, + { + "countryCode": "MW", + "countryName": "Malawi", + "currencyCode": "MWK" + }, + { + "countryCode": "MX", + "countryName": "Mexico", + "currencyCode": "MXN" + }, + { + "countryCode": "MY", + "countryName": "Malaysia", + "currencyCode": "MYR" + }, + { + "countryCode": "MZ", + "countryName": "Mozambique", + "currencyCode": "MZN" + }, + { + "countryCode": "NA", + "countryName": "Namibia", + "currencyCode": "NAD" + }, + { + "countryCode": "NC", + "countryName": "New Caledonia", + "currencyCode": "XPF" + }, + { + "countryCode": "NE", + "countryName": "Niger", + "currencyCode": "XOF" + }, + { + "countryCode": "NF", + "countryName": "Norfolk Island", + "currencyCode": "AUD" + }, + { + "countryCode": "NG", + "countryName": "Nigeria", + "currencyCode": "NGN" + }, + { + "countryCode": "NI", + "countryName": "Nicaragua", + "currencyCode": "NIO" + }, + { + "countryCode": "NL", + "countryName": "Netherlands", + "currencyCode": "EUR" + }, + { + "countryCode": "NO", + "countryName": "Norway", + "currencyCode": "NOK" + }, + { + "countryCode": "NP", + "countryName": "Nepal", + "currencyCode": "NPR" + }, + { + "countryCode": "NR", + "countryName": "Nauru", + "currencyCode": "AUD" + }, + { + "countryCode": "NU", + "countryName": "Niue", + "currencyCode": "NZD" + }, + { + "countryCode": "NZ", + "countryName": "New Zealand", + "currencyCode": "NZD" + }, + { + "countryCode": "OM", + "countryName": "Oman", + "currencyCode": "OMR" + }, + { + "countryCode": "PA", + "countryName": "Panama", + "currencyCode": "PAB" + }, + { + "countryCode": "PE", + "countryName": "Peru", + "currencyCode": "PEN" + }, + { + "countryCode": "PF", + "countryName": "French Polynesia", + "currencyCode": "XPF" + }, + { + "countryCode": "PG", + "countryName": "Papua New Guinea", + "currencyCode": "PGK" + }, + { + "countryCode": "PH", + "countryName": "Philippines", + "currencyCode": "PHP" + }, + { + "countryCode": "PK", + "countryName": "Pakistan", + "currencyCode": "PKR" + }, + { + "countryCode": "PL", + "countryName": "Poland", + "currencyCode": "PLN" + }, + { + "countryCode": "PM", + "countryName": "Saint Pierre and Miquelon", + "currencyCode": "EUR" + }, + { + "countryCode": "PN", + "countryName": "Pitcairn Islands", + "currencyCode": "NZD" + }, + { + "countryCode": "PR", + "countryName": "Puerto Rico", + "currencyCode": "USD" + }, + { + "countryCode": "PS", + "countryName": "Palestine", + "currencyCode": "ILS" + }, + { + "countryCode": "PT", + "countryName": "Portugal", + "currencyCode": "EUR" + }, + { + "countryCode": "PW", + "countryName": "Palau", + "currencyCode": "USD" + }, + { + "countryCode": "PY", + "countryName": "Paraguay", + "currencyCode": "PYG" + }, + { + "countryCode": "QA", + "countryName": "Qatar", + "currencyCode": "QAR" + }, + { + "countryCode": "RE", + "countryName": "Réunion", + "currencyCode": "EUR" + }, + { + "countryCode": "RO", + "countryName": "Romania", + "currencyCode": "RON" + }, + { + "countryCode": "RS", + "countryName": "Serbia", + "currencyCode": "RSD" + }, + { + "countryCode": "RU", + "countryName": "Russia", + "currencyCode": "RUB" + }, + { + "countryCode": "RW", + "countryName": "Rwanda", + "currencyCode": "RWF" + }, + { + "countryCode": "SA", + "countryName": "Saudi Arabia", + "currencyCode": "SAR" + }, + { + "countryCode": "SB", + "countryName": "Solomon Islands", + "currencyCode": "SBD" + }, + { + "countryCode": "SC", + "countryName": "Seychelles", + "currencyCode": "SCR" + }, + { + "countryCode": "SD", + "countryName": "Sudan", + "currencyCode": "SDG" + }, + { + "countryCode": "SE", + "countryName": "Sweden", + "currencyCode": "SEK" + }, + { + "countryCode": "SG", + "countryName": "Singapore", + "currencyCode": "SGD" + }, + { + "countryCode": "SH", + "countryName": "Saint Helena", + "currencyCode": "SHP" + }, + { + "countryCode": "SI", + "countryName": "Slovenia", + "currencyCode": "EUR" + }, + { + "countryCode": "SJ", + "countryName": "Svalbard and Jan Mayen", + "currencyCode": "NOK" + }, + { + "countryCode": "SK", + "countryName": "Slovakia", + "currencyCode": "EUR" + }, + { + "countryCode": "SL", + "countryName": "Sierra Leone", + "currencyCode": "SLL" + }, + { + "countryCode": "SM", + "countryName": "San Marino", + "currencyCode": "EUR" + }, + { + "countryCode": "SN", + "countryName": "Senegal", + "currencyCode": "XOF" + }, + { + "countryCode": "SO", + "countryName": "Somalia", + "currencyCode": "SOS" + }, + { + "countryCode": "SR", + "countryName": "Suriname", + "currencyCode": "SRD" + }, + { + "countryCode": "SS", + "countryName": "South Sudan", + "currencyCode": "SSP" + }, + { + "countryCode": "ST", + "countryName": "São Tomé and Príncipe", + "currencyCode": "STD" + }, + { + "countryCode": "SV", + "countryName": "El Salvador", + "currencyCode": "USD" + }, + { + "countryCode": "SX", + "countryName": "Sint Maarten", + "currencyCode": "ANG" + }, + { + "countryCode": "SY", + "countryName": "Syria", + "currencyCode": "SYP" + }, + { + "countryCode": "SZ", + "countryName": "Swaziland", + "currencyCode": "SZL" + }, + { + "countryCode": "TC", + "countryName": "Turks and Caicos Islands", + "currencyCode": "USD" + }, + { + "countryCode": "TD", + "countryName": "Chad", + "currencyCode": "XAF" + }, + { + "countryCode": "TF", + "countryName": "French Southern Territories", + "currencyCode": "EUR" + }, + { + "countryCode": "TG", + "countryName": "Togo", + "currencyCode": "XOF" + }, + { + "countryCode": "TH", + "countryName": "Thailand", + "currencyCode": "THB" + }, + { + "countryCode": "TJ", + "countryName": "Tajikistan", + "currencyCode": "TJS" + }, + { + "countryCode": "TK", + "countryName": "Tokelau", + "currencyCode": "NZD" + }, + { + "countryCode": "TL", + "countryName": "East Timor", + "currencyCode": "USD" + }, + { + "countryCode": "TM", + "countryName": "Turkmenistan", + "currencyCode": "TMT" + }, + { + "countryCode": "TN", + "countryName": "Tunisia", + "currencyCode": "TND" + }, + { + "countryCode": "TO", + "countryName": "Tonga", + "currencyCode": "TOP" + }, + { + "countryCode": "TR", + "countryName": "Turkey", + "currencyCode": "TRY" + }, + { + "countryCode": "TT", + "countryName": "Trinidad and Tobago", + "currencyCode": "TTD" + }, + { + "countryCode": "TV", + "countryName": "Tuvalu", + "currencyCode": "AUD" + }, + { + "countryCode": "TW", + "countryName": "Taiwan", + "currencyCode": "TWD" + }, + { + "countryCode": "TZ", + "countryName": "Tanzania", + "currencyCode": "TZS" + }, + { + "countryCode": "UA", + "countryName": "Ukraine", + "currencyCode": "UAH" + }, + { + "countryCode": "UG", + "countryName": "Uganda", + "currencyCode": "UGX" + }, + { + "countryCode": "UM", + "countryName": "U.S. Minor Outlying Islands", + "currencyCode": "USD" + }, + { + "countryCode": "US", + "countryName": "United States", + "currencyCode": "USD" + }, + { + "countryCode": "UY", + "countryName": "Uruguay", + "currencyCode": "UYU" + }, + { + "countryCode": "UZ", + "countryName": "Uzbekistan", + "currencyCode": "UZS" + }, + { + "countryCode": "VA", + "countryName": "Vatican City", + "currencyCode": "EUR" + }, + { + "countryCode": "VC", + "countryName": "Saint Vincent and the Grenadines", + "currencyCode": "XCD" + }, + { + "countryCode": "VE", + "countryName": "Venezuela", + "currencyCode": "VEF" + }, + { + "countryCode": "VG", + "countryName": "British Virgin Islands", + "currencyCode": "USD" + }, + { + "countryCode": "VI", + "countryName": "U.S. Virgin Islands", + "currencyCode": "USD" + }, + { + "countryCode": "VN", + "countryName": "Vietnam", + "currencyCode": "VND" + }, + { + "countryCode": "VU", + "countryName": "Vanuatu", + "currencyCode": "VUV" + }, + { + "countryCode": "WF", + "countryName": "Wallis and Futuna", + "currencyCode": "XPF" + }, + { + "countryCode": "WS", + "countryName": "Samoa", + "currencyCode": "WST" + }, + { + "countryCode": "XK", + "countryName": "Kosovo", + "currencyCode": "EUR" + }, + { + "countryCode": "YE", + "countryName": "Yemen", + "currencyCode": "YER" + }, + { + "countryCode": "YT", + "countryName": "Mayotte", + "currencyCode": "EUR" + }, + { + "countryCode": "ZA", + "countryName": "South Africa", + "currencyCode": "ZAR" + }, + { + "countryCode": "ZM", + "countryName": "Zambia", + "currencyCode": "ZMW" + }, + { + "countryCode": "ZW", + "countryName": "Zimbabwe", + "currencyCode": "ZWL" + } + ] +} \ No newline at end of file diff --git a/services/module-cms/functions/countries/country.components.ts b/services/module-cms/functions/countries/country.components.ts new file mode 100644 index 0000000..4594403 --- /dev/null +++ b/services/module-cms/functions/countries/country.components.ts @@ -0,0 +1,177 @@ +import { registerEnumType } from 'type-graphql'; + +export enum CurrencyCodesComponentEnum { + 'AED' = 'AED', + 'AFN' = 'AFN', + 'ALL' = 'ALL', + 'AMD' = 'AMD', + 'ANG' = 'ANG', + 'AOA' = 'AOA', + 'ARS' = 'ARS', + 'AUD' = 'AUD', + 'AWG' = 'AWG', + 'AZN' = 'AZN', + 'BAM' = 'BAM', + 'BBD' = 'BBD', + 'BDT' = 'BDT', + 'BGN' = 'BGN', + 'BHD' = 'BHD', + 'BIF' = 'BIF', + 'BMD' = 'BMD', + 'BND' = 'BND', + 'BOB' = 'BOB', + 'BRL' = 'BRL', + 'BSD' = 'BSD', + 'BTC' = 'BTC', + 'BTN' = 'BTN', + 'BWP' = 'BWP', + 'BYN' = 'BYN', + 'BZD' = 'BZD', + 'CAD' = 'CAD', + 'CDF' = 'CDF', + 'CHF' = 'CHF', + 'CLF' = 'CLF', + 'CLP' = 'CLP', + 'CNH' = 'CNH', + 'CNY' = 'CNY', + 'COP' = 'COP', + 'CRC' = 'CRC', + 'CUC' = 'CUC', + 'CUP' = 'CUP', + 'CVE' = 'CVE', + 'CZK' = 'CZK', + 'DJF' = 'DJF', + 'DKK' = 'DKK', + 'DOP' = 'DOP', + 'DZD' = 'DZD', + 'EGP' = 'EGP', + 'ERN' = 'ERN', + 'ETB' = 'ETB', + 'EUR' = 'EUR', + 'FJD' = 'FJD', + 'FKP' = 'FKP', + 'GBP' = 'GBP', + 'GEL' = 'GEL', + 'GGP' = 'GGP', + 'GHS' = 'GHS', + 'GIP' = 'GIP', + 'GMD' = 'GMD', + 'GNF' = 'GNF', + 'GTQ' = 'GTQ', + 'GYD' = 'GYD', + 'HKD' = 'HKD', + 'HNL' = 'HNL', + 'HRK' = 'HRK', + 'HTG' = 'HTG', + 'HUF' = 'HUF', + 'IDR' = 'IDR', + 'ILS' = 'ILS', + 'IMP' = 'IMP', + 'INR' = 'INR', + 'IQD' = 'IQD', + 'IRR' = 'IRR', + 'ISK' = 'ISK', + 'JEP' = 'JEP', + 'JMD' = 'JMD', + 'JOD' = 'JOD', + 'JPY' = 'JPY', + 'KES' = 'KES', + 'KGS' = 'KGS', + 'KHR' = 'KHR', + 'KMF' = 'KMF', + 'KPW' = 'KPW', + 'KRW' = 'KRW', + 'KWD' = 'KWD', + 'KYD' = 'KYD', + 'KZT' = 'KZT', + 'LAK' = 'LAK', + 'LBP' = 'LBP', + 'LKR' = 'LKR', + 'LRD' = 'LRD', + 'LSL' = 'LSL', + 'LYD' = 'LYD', + 'MAD' = 'MAD', + 'MDL' = 'MDL', + 'MGA' = 'MGA', + 'MKD' = 'MKD', + 'MMK' = 'MMK', + 'MNT' = 'MNT', + 'MOP' = 'MOP', + 'MRO' = 'MRO', + 'MRU' = 'MRU', + 'MUR' = 'MUR', + 'MVR' = 'MVR', + 'MWK' = 'MWK', + 'MXN' = 'MXN', + 'MYR' = 'MYR', + 'MZN' = 'MZN', + 'NAD' = 'NAD', + 'NGN' = 'NGN', + 'NIO' = 'NIO', + 'NOK' = 'NOK', + 'NPR' = 'NPR', + 'NZD' = 'NZD', + 'OMR' = 'OMR', + 'PAB' = 'PAB', + 'PEN' = 'PEN', + 'PGK' = 'PGK', + 'PHP' = 'PHP', + 'PKR' = 'PKR', + 'PLN' = 'PLN', + 'PYG' = 'PYG', + 'QAR' = 'QAR', + 'RON' = 'RON', + 'RSD' = 'RSD', + 'RUB' = 'RUB', + 'RWF' = 'RWF', + 'SAR' = 'SAR', + 'SBD' = 'SBD', + 'SCR' = 'SCR', + 'SDG' = 'SDG', + 'SEK' = 'SEK', + 'SGD' = 'SGD', + 'SHP' = 'SHP', + 'SLL' = 'SLL', + 'SOS' = 'SOS', + 'SRD' = 'SRD', + 'SSP' = 'SSP', + 'STD' = 'STD', + 'STN' = 'STN', + 'SVC' = 'SVC', + 'SYP' = 'SYP', + 'SZL' = 'SZL', + 'THB' = 'THB', + 'TJS' = 'TJS', + 'TMT' = 'TMT', + 'TND' = 'TND', + 'TOP' = 'TOP', + 'TRY' = 'TRY', + 'TTD' = 'TTD', + 'TWD' = 'TWD', + 'TZS' = 'TZS', + 'UAH' = 'UAH', + 'UGX' = 'UGX', + 'USD' = 'USD', + 'UYU' = 'UYU', + 'UZS' = 'UZS', + 'VEF' = 'VEF', + 'VND' = 'VND', + 'VUV' = 'VUV', + 'WST' = 'WST', + 'XAF' = 'XAF', + 'XAG' = 'XAG', + 'XAU' = 'XAU', + 'XCD' = 'XCD', + 'XDR' = 'XDR', + 'XOF' = 'XOF', + 'XPD' = 'XPD', + 'XPF' = 'XPF', + 'XPT' = 'XPT', + 'YER' = 'YER', + 'ZAR' = 'ZAR', + 'ZMW' = 'ZMW', + 'ZWL' = 'ZWL', +} +registerEnumType(CurrencyCodesComponentEnum, { + name: 'CurrencyCodesComponentEnum', +}); diff --git a/services/module-cms/functions/countries/country.model.ts b/services/module-cms/functions/countries/country.model.ts new file mode 100644 index 0000000..0299e5f --- /dev/null +++ b/services/module-cms/functions/countries/country.model.ts @@ -0,0 +1,41 @@ +import { ObjectType } from 'type-graphql'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { EngineModel } from '@lib/seed/engine/EngineModel'; +import { plainToClass } from 'class-transformer'; +import { CustomSearchEngine } from '@seed/services/database/DBRequestService'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import { CountryDBInterfaceSchema, CountryDBSchema, CountrySchema } from './schemas/country.schema'; +import { CountryArgsSchema } from './schemas/country.input'; + +export const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin, AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class CountryModel extends EngineModel { + public constructor(input?: CountryDBInterfaceSchema & Partial) { + const dataInit = plainToClass(CountryDBSchema, input || {}); + super({ + // ...init, + collectionName: 'countries', + permissions: permissions, + dataInit, + }); + } + + plainToClass(plain: any): CountrySchema | CountrySchema[] { + return plainToClass(CountrySchema, plain); + } + + searchEngine(): CustomSearchEngine { + const sEngine: CustomSearchEngine = {}; + + return sEngine; + } +} diff --git a/services/module-cms/functions/countries/country.service.ts b/services/module-cms/functions/countries/country.service.ts new file mode 100644 index 0000000..8108d19 --- /dev/null +++ b/services/module-cms/functions/countries/country.service.ts @@ -0,0 +1,28 @@ +import countries from './countries.json'; +import { CountrySchema } from './schemas/country.schema'; +import { v4 as uuid } from 'uuid'; +import { permissions } from './country.model'; +import { ApolloContext } from '@seed/interfaces/context'; + +const formatData = (data: any, ctx): CountrySchema => { + const account = ctx.ctx.user; + const formatedData: any = { ...data, _id: uuid(), createdAt: new Date(), by: account._id, updatedAt: new Date(), permissions }; + return formatedData; +}; + +export const importFromJSON = (ctx: ApolloContext): any => { + const formatedCountries = countries.countries.map((country) => { + return formatData( + { + currencyCodes: [country.currencyCode], + countryCode: country.countryCode, + countryName: { + en: country.countryName, + }, + }, + ctx, + ); + }); + + return formatedCountries; +}; diff --git a/services/module-cms/functions/countries/resolvers/country.resolver.admin.ts b/services/module-cms/functions/countries/resolvers/country.resolver.admin.ts new file mode 100644 index 0000000..1170fc6 --- /dev/null +++ b/services/module-cms/functions/countries/resolvers/country.resolver.admin.ts @@ -0,0 +1,73 @@ +import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'; + +import CountryModel from '../country.model'; +import { createEngineQueryResolver, createEngineMutationResolver } from '@seed/engine/genericResolvers/BaseEngineResolver'; +import { CountryArgsSchema, CountryEditInputSchema, CountryNewInputSchema } from '../schemas/country.input'; +import { CountrySchema } from '../schemas/country.schema'; +import { ApolloContext } from '@seed/interfaces/context'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { importFromJSON } from '../country.service'; +import { SimpleResult } from '@seed/interfaces/components'; + +const CountryGenericQueryResolver = createEngineQueryResolver({ + domain: 'country', + schemaName: CountrySchema, + modelName: CountryModel, + argsType: CountryArgsSchema, + engineMiddleware: { + noPermissionCheck: false, + authorization: [AccountTypeEnum.admin], + }, +}); + +const CountryGenericMutationResolver = createEngineMutationResolver({ + domain: 'country', + schemaName: CountrySchema, + modelName: CountryModel, + editInput: CountryEditInputSchema, + newInput: CountryNewInputSchema, + engineMiddleware: { + noPermissionCheck: false, + authorization: [AccountTypeEnum.admin], + }, +}); + +@Resolver(CountrySchema) +export class CountryQueryResolver extends CountryGenericQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ +} + +@Resolver(CountrySchema) +export class CountryMutationResolver extends CountryGenericMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => SimpleResult) + @Authorized(AccountTypeEnum.admin) + async countryImportMany(@Ctx() ctx: ApolloContext): Promise { + const dataToImport: CountrySchema[] = importFromJSON(ctx); + const model = new CountryModel(); + + try { + await (await model.db()).insertMany(dataToImport, { ordered: false }); + return { message: 'imported' }; + } catch (error) { + throw error; + } + } +} + +export const CountryAdminResolvers = [CountryQueryResolver, CountryMutationResolver]; diff --git a/services/module-cms/functions/countries/resolvers/country.resolver.ts b/services/module-cms/functions/countries/resolvers/country.resolver.ts new file mode 100644 index 0000000..f3ea3a3 --- /dev/null +++ b/services/module-cms/functions/countries/resolvers/country.resolver.ts @@ -0,0 +1,44 @@ +import { Resolver } from 'type-graphql'; + +import CountryModel from '../country.model'; +import { createEngineQueryResolver } from '@seed/engine/genericResolvers/BaseEngineResolver'; +import { CountryArgsSchema } from '../schemas/country.input'; +import { CountrySchema } from '../schemas/country.schema'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const CountryGenericQueryResolver = createEngineQueryResolver({ + domain: 'countries', + schemaName: CountrySchema, + modelName: CountryModel, + argsType: CountryArgsSchema, + engineMiddleware: { + noPermissionCheck: false, + authorization: [AccountTypeEnum.public], + }, +}); + +@Resolver(CountrySchema) +export class CountryQueryResolver extends CountryGenericQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ +} + +@Resolver(CountrySchema) +export class CountryMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} + +export const CountryAppResolvers = [CountryQueryResolver]; diff --git a/services/module-cms/functions/countries/schemas/country.input.ts b/services/module-cms/functions/countries/schemas/country.input.ts new file mode 100644 index 0000000..caba921 --- /dev/null +++ b/services/module-cms/functions/countries/schemas/country.input.ts @@ -0,0 +1,34 @@ +import { InputType, ArgsType, Field } from 'type-graphql'; + +import { GetManyArgs } from '@seed/graphql/Request'; +import { CountryBaseSchema } from './country.schema'; +import { CountryCodesComponentEnum } from '@seed/services/geolocation/components/countries'; +import { TranslatableComponent } from '@src/__components/components'; +import { CurrencyCodesComponentEnum } from '../country.components'; + +@InputType() +export class CountryNewInputSchema extends CountryBaseSchema { + @Field(() => TranslatableComponent) + countryName: TranslatableComponent; + + @Field(() => CountryCodesComponentEnum) + countryCode: CountryCodesComponentEnum; + + @Field(() => [CurrencyCodesComponentEnum]) + currencyCodes: CurrencyCodesComponentEnum[]; +} + +@InputType() +export class CountryEditInputSchema implements Partial { + @Field(() => TranslatableComponent, { nullable: true }) + countryName?: TranslatableComponent; + + @Field(() => CountryCodesComponentEnum, { nullable: true }) + countryCode?: CountryCodesComponentEnum; + + @Field(() => [CurrencyCodesComponentEnum], { nullable: true }) + currencyCodes?: CurrencyCodesComponentEnum[]; +} + +@ArgsType() +export class CountryArgsSchema extends GetManyArgs {} diff --git a/services/module-cms/functions/countries/schemas/country.schema.ts b/services/module-cms/functions/countries/schemas/country.schema.ts new file mode 100644 index 0000000..6be84b0 --- /dev/null +++ b/services/module-cms/functions/countries/schemas/country.schema.ts @@ -0,0 +1,34 @@ +import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema'; +import { Field, InputType, ObjectType } from 'type-graphql'; +import { CountryCodesComponentEnum } from '@seed/services/geolocation/components/countries'; +import { TranslatableComponent } from '@src/__components/components'; +import { CurrencyCodesComponentEnum } from '../country.components'; + +@InputType() +@ObjectType() +export class CountryBaseSchema { + @Field(() => TranslatableComponent) + countryName: TranslatableComponent; + + @Field(() => CountryCodesComponentEnum) + countryCode: CountryCodesComponentEnum; + + @Field(() => [CurrencyCodesComponentEnum]) + currencyCodes: CurrencyCodesComponentEnum[]; +} + +@ObjectType() +export class CountryDBInterfaceSchema extends CountryBaseSchema {} + +@ObjectType() +export class CountryDBSchema extends CountryDBInterfaceSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class CountrySchema extends CountryDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/services/module-cms/functions/emails-settings/emails-settings.components.ts b/services/module-cms/functions/emails-settings/emails-settings.components.ts new file mode 100644 index 0000000..bb1f26f --- /dev/null +++ b/services/module-cms/functions/emails-settings/emails-settings.components.ts @@ -0,0 +1,57 @@ +import { AvailableTranslation, TranslatableComponent } from '@src/__components/components'; +import { Field, InputType, ObjectType, registerEnumType } from 'type-graphql'; + +@ObjectType() +@InputType('PushNotifComponentInput') +export class PushNotifComponent { + @Field(() => TranslatableComponent, { nullable: true }) + subject?: TranslatableComponent; + + @Field(() => TranslatableComponent) + text: TranslatableComponent; +} + +@ObjectType() +@InputType('SmsSettingsComponentInput') +export class SmsSettingsComponent { + @Field(() => TranslatableComponent) + text: TranslatableComponent; +} + +export enum SlackActionStyleEnum { + primary = 'primary', + danger = 'danger', +} +registerEnumType(SlackActionStyleEnum, { + name: 'SlackActionStyleEnum', +}); + +@ObjectType() +@InputType('SlackActionInfoInput') +export class SlackActionInfo { + @Field() + text: string; + @Field() + url: string; + @Field(() => SlackActionStyleEnum, { nullable: true }) + style?: SlackActionStyleEnum; +} + +@ObjectType() +@InputType('SlackSettingsComponentInput') +export class SlackSettingsComponent { + @Field(() => [String]) + webhookUrls: string[]; + + @Field({ nullable: true }) + username?: string; + + @Field(() => String) + text: string; + + @Field({ nullable: true }) + icon?: string; + + @Field(() => [SlackActionInfo], { nullable: true }) + actions?: SlackActionInfo[]; +} diff --git a/services/module-cms/functions/emails-settings/emails-settings.model.ts b/services/module-cms/functions/emails-settings/emails-settings.model.ts new file mode 100644 index 0000000..4f123ba --- /dev/null +++ b/services/module-cms/functions/emails-settings/emails-settings.model.ts @@ -0,0 +1,191 @@ +import { ObjectType, Field, ID, ArgsType, InputType } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetArgs, GetManyArgs } from '@seed/graphql/Request'; + +import { SettingsType } from '@seed/interfaces/components'; +import { RessourceEnum, TranslatableComponent } from '@src/__components/components'; +import { ApolloError } from 'apollo-server-lambda'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import _ from 'lodash'; +import { PushNotifComponent, SlackSettingsComponent, SmsSettingsComponent } from './emails-settings.components'; +import { newError } from '@seed/helpers/Error'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class EmailSettingsModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'settings', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field(() => SettingsType) + type = SettingsType.emails; + + @Field() + key: string; + + @Field(() => TranslatableComponent, { nullable: true }) + subject?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + body?: TranslatableComponent; + + // Related to EMAIL + @Field() + custom: boolean; + @Field(() => TranslatableComponent, { nullable: true }) + templateId?: TranslatableComponent; + @Field() + fromEmail: string; + @Field() + fromName: string; + @Field() + replyToEmail: string; + @Field(() => [String], { nullable: true }) + cci: string[]; + + @Field(() => [String], { nullable: true }) + availableFields?: string[]; + + @Field(() => String, { nullable: true }) + templateSample?: string; + + /* + SMS + */ + + @Field(() => SmsSettingsComponent, { nullable: true }) + smsSettings?: SmsSettingsComponent; + + /* + PUSH NOTIFICATION + */ + + @Field(() => PushNotifComponent, { nullable: true }) + pushSettings?: PushNotifComponent; + + /* + SLACK + */ + @Field(() => SlackSettingsComponent, { nullable: true }) + slackSettings?: SlackSettingsComponent; + + searchOptions(): string[] { + return ['key', 'type']; + } + filterOptions(): string[] { + return ['key', 'type']; + } + + onCreate(input: NewEmailSettingsInput): void { + if (input.custom == false) { + if (!input.subject || !input.body) throw newError(3000, { message: 'subject and body must be required when not custom' }); + } + return; + } +} + +@InputType() +export class NewEmailSettingsInput { + @Field(() => TranslatableComponent, { nullable: true }) + subject: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + body: TranslatableComponent; + + @Field(() => String) + type = SettingsType.emails; + + @Field() + key: string; + + @Field({ nullable: true }) + custom: boolean; + + @Field(() => TranslatableComponent, { nullable: true }) + templateId?: TranslatableComponent; + @Field() + fromEmail: string; + @Field() + fromName: string; + @Field() + replyToEmail: string; + @Field(() => [String], { nullable: true }) + cci: string[]; + + /* + SMS + */ + + @Field(() => SmsSettingsComponent, { nullable: true }) + smsSettings?: SmsSettingsComponent; + + /* + SLACK + */ + @Field(() => SlackSettingsComponent, { nullable: true }) + slackSettings?: SlackSettingsComponent; +} + +@InputType() +export class EditEmailSettingsInput { + @Field(() => TranslatableComponent, { nullable: true }) + subject: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + body: TranslatableComponent; + + @Field(() => String) + type = SettingsType.emails; + + @Field({ nullable: true }) + custom: boolean; + + @Field(() => TranslatableComponent, { nullable: true }) + templateId?: TranslatableComponent; + + @Field() + fromEmail: string; + @Field() + fromName: string; + @Field() + replyToEmail: string; + @Field(() => [String], { nullable: true }) + cci: string[]; + + /* + SMS + */ + + @Field(() => SmsSettingsComponent, { nullable: true }) + smsSettings?: SmsSettingsComponent; + + /* + SLACK + */ + @Field(() => SlackSettingsComponent, { nullable: true }) + slackSettings?: SlackSettingsComponent; +} + +@ArgsType() +export class EmailSettingsArgs { + @Field({ nullable: true }) + search?: string; + + type = SettingsType.emails; + + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} diff --git a/services/module-cms/functions/emails-settings/emails-settings.resolver.ts b/services/module-cms/functions/emails-settings/emails-settings.resolver.ts new file mode 100644 index 0000000..8d5c279 --- /dev/null +++ b/services/module-cms/functions/emails-settings/emails-settings.resolver.ts @@ -0,0 +1,33 @@ +import { Resolver, Query, Args, Authorized, FieldResolver, Root, Ctx } from 'type-graphql'; +import EmailSettingsModel, { EditEmailSettingsInput, EmailSettingsArgs, NewEmailSettingsInput } from './emails-settings.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; + +const EmailSettingBaseResolver = createBaseResolver( + 'emailsSettings', + EmailSettingsModel, + EmailSettingsArgs, + NewEmailSettingsInput, + EditEmailSettingsInput, + [AccountTypeEnum.admin], +); + +@Resolver(EmailSettingsModel) +export default class EmailSettingsAdminResolver extends EmailSettingBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-cms/functions/error-settings/errors-settings.components.ts b/services/module-cms/functions/error-settings/errors-settings.components.ts new file mode 100644 index 0000000..e69de29 diff --git a/services/module-cms/functions/error-settings/errors-settings.model.ts b/services/module-cms/functions/error-settings/errors-settings.model.ts new file mode 100644 index 0000000..101de69 --- /dev/null +++ b/services/module-cms/functions/error-settings/errors-settings.model.ts @@ -0,0 +1,91 @@ +import { ObjectType, Field, ID, ArgsType, InputType } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetArgs } from '@seed/graphql/Request'; + +import { SettingsType } from '@seed/interfaces/components'; +import { TranslatableComponent } from '@src/__components/components'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class ErrorSettingsModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'settings', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field(() => SettingsType) + type = SettingsType.errors; + + @Field() + key: string; + + @Field() + value: string; + + @Field(() => TranslatableComponent, { nullable: true }) + subject?: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + body: TranslatableComponent; + + @Field(() => [String], { nullable: true }) + availableFields?: string[]; + + searchOptions(): string[] { + return ['key', 'type']; + } + filterOptions(): string[] { + return ['key', 'type']; + } +} + +@InputType() +export class NewErrorSettingsInput { + @Field(() => TranslatableComponent, { nullable: true }) + subject?: TranslatableComponent; + @Field(() => TranslatableComponent) + body: TranslatableComponent; + + @Field(() => String) + type = SettingsType.errors; + + @Field() + key: string; + + @Field() + value: string; +} + +@InputType() +export class EditErrorSettingsInput { + @Field(() => TranslatableComponent, { nullable: true }) + subject: TranslatableComponent; + @Field(() => TranslatableComponent, { nullable: true }) + body: TranslatableComponent; +} + +@ArgsType() +export class ErrorSettingsArgs { + @Field({ nullable: true }) + search?: string; + + type = SettingsType.errors; + + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} diff --git a/services/module-cms/functions/error-settings/errors-settings.resolver.ts b/services/module-cms/functions/error-settings/errors-settings.resolver.ts new file mode 100644 index 0000000..97445e7 --- /dev/null +++ b/services/module-cms/functions/error-settings/errors-settings.resolver.ts @@ -0,0 +1,33 @@ +import { Resolver } from 'type-graphql'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +import ErrorSettingsModel, { EditErrorSettingsInput, ErrorSettingsArgs, NewErrorSettingsInput } from './errors-settings.model'; + +const ErrorSettingsBaseResolver = createBaseResolver( + 'errorSettings', + ErrorSettingsModel, + ErrorSettingsArgs, + NewErrorSettingsInput, + EditErrorSettingsInput, + [AccountTypeEnum.admin], +); + +@Resolver(ErrorSettingsModel) +export default class ErrorSettingsAdminResolver extends ErrorSettingsBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-cms/functions/lists/list.model.ts b/services/module-cms/functions/lists/list.model.ts new file mode 100644 index 0000000..0e5df1a --- /dev/null +++ b/services/module-cms/functions/lists/list.model.ts @@ -0,0 +1,75 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { GetArgs } from '@seed/graphql/Request'; + +import { TranslatableComponent, ListEnum } from '@src/__components/components'; + +import { Permission } from '@seed/interfaces/permission'; +import BaseSEOModel, { EditBaseSEOInput, NewBaseSEOInput } from '@seed/graphql/baseModels/BaseSeoModel'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class ListModel extends BaseSEOModel { + public constructor() { + super('lists', permissions); + } + + @Field(() => ListEnum) + ressourceType: ListEnum; + + @Field(() => TranslatableComponent) + title: TranslatableComponent; + + @Field({ nullable: true }) + colorCode?: string; + + searchOptions(): string[] { + return ['ressourceType']; + } + filterOptions(): string[] { + return ['ressourceType']; + } +} + +@ArgsType() +export class ListArgs { + @Field({ nullable: true }) + search?: string; + + @Field(() => ListEnum) + ressourceType: ListEnum; + + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@InputType() +export class NewListInput extends NewBaseSEOInput implements Partial { + @Field(() => ListEnum) + ressourceType: ListEnum; + + @Field(() => TranslatableComponent) + title: TranslatableComponent; + + @Field({ nullable: true }) + colorCode?: string; +} + +@InputType() +export class EditListInput extends EditBaseSEOInput implements Partial { + @Field(() => ListEnum, { nullable: true }) + ressourceType?: ListEnum; + + @Field(() => TranslatableComponent, { nullable: true }) + title?: TranslatableComponent; + + @Field({ nullable: true }) + colorCode?: string; +} diff --git a/services/module-cms/functions/lists/list.resolver.admin.ts b/services/module-cms/functions/lists/list.resolver.admin.ts new file mode 100644 index 0000000..26c6cab --- /dev/null +++ b/services/module-cms/functions/lists/list.resolver.admin.ts @@ -0,0 +1,26 @@ +import { Resolver } from 'type-graphql'; +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import ListModel, { EditListInput, ListArgs, NewListInput } from './list.model'; + +const ListAdminBaseResolver = createBaseResolver('lists', ListModel, ListArgs, NewListInput, EditListInput, [AccountTypeEnum.admin]); + +@Resolver(ListModel) +export default class ListAdminResolver extends ListAdminBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-cms/functions/lists/list.resolver.fields.ts b/services/module-cms/functions/lists/list.resolver.fields.ts new file mode 100644 index 0000000..8e3e3d2 --- /dev/null +++ b/services/module-cms/functions/lists/list.resolver.fields.ts @@ -0,0 +1,15 @@ +import { Resolver } from 'type-graphql'; + +import ListModel from './list.model'; + +@Resolver(ListModel) +export default class ListFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-cms/functions/lists/list.resolver.ts b/services/module-cms/functions/lists/list.resolver.ts new file mode 100644 index 0000000..4ec5a99 --- /dev/null +++ b/services/module-cms/functions/lists/list.resolver.ts @@ -0,0 +1,26 @@ +import { Resolver } from 'type-graphql'; + +import { createBasePublicResolver } from '@seed/graphql/baseResolvers/BasePublicResolver'; +import ListModel, { ListArgs } from './list.model'; + +const ListBaseResolver = createBasePublicResolver('lists', ListModel, ListArgs); + +@Resolver(ListModel) +export default class ListResolver extends ListBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-cms/functions/pages/pages.model.ts b/services/module-cms/functions/pages/pages.model.ts new file mode 100644 index 0000000..5900639 --- /dev/null +++ b/services/module-cms/functions/pages/pages.model.ts @@ -0,0 +1,51 @@ +import { ObjectType, Field, ID, ArgsType, InputType, registerEnumType, Int } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetArgs } from '@seed/graphql/Request'; + +import { GraphQLJSONObject } from 'graphql-type-json'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { EngineModel } from '@seed/engine/EngineModel'; +import { PageDBInterfaceSchema, PageDBSchema, PageSchema } from './schemas/pages.schema'; +import { plainToClass } from 'class-transformer'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import { uploadToS3 } from '@services/api-uploads/services/S3Service'; +import { updateCaching } from '@services/module-cms/services/PageService'; +import { StreamDBSchema } from '@seed/engine/utils/streams/schemas/stream.schema'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class PageModel extends EngineModel { + public constructor(input?: PageDBInterfaceSchema & Partial) { + const dataInit = plainToClass(PageDBSchema, input || {}); + super({ + collectionName: 'pages', + permissions: permissions, + dataInit, + }); + } + + plainToClass(plain: any): PageSchema | PageSchema[] { + return plainToClass(PageSchema, plain); + } + + async afterCreate(): Promise { + await updateCaching(this); + } + + async afterUpdate(): Promise { + await updateCaching(this); + } + + async afterDelete(): Promise { + await updateCaching(this); + } +} diff --git a/services/module-cms/functions/pages/pages.resolver.admin.ts b/services/module-cms/functions/pages/pages.resolver.admin.ts new file mode 100644 index 0000000..7680b2d --- /dev/null +++ b/services/module-cms/functions/pages/pages.resolver.admin.ts @@ -0,0 +1,72 @@ +import { Resolver, Query, Args, Authorized, Ctx } from 'type-graphql'; +import { Mutation, Arg } from 'type-graphql'; + +import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric } from '@seed/graphql/BaseService'; + +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { createEngineMutationResolver, createEngineQueryResolver } from '@seed/engine/genericResolvers/BaseEngineResolver'; +import { PageSchema } from './schemas/pages.schema'; +import { PageArgs, NewPageInput, EditPageInput } from './schemas/pages.input'; +import PageModel from './pages.model'; +import { SuccessResponse } from '@seed/interfaces/response'; +import { EngineMiddleware } from '@seed/graphql/MiddlewareV2'; +import { updateCaching as updatePagesCaching } from '@services/module-cms/services/PageService'; +import { ApolloContext } from '@seed/interfaces/context'; + +const PageBaseQueryAdminResolver = createEngineQueryResolver({ + domain: 'pages', + schemaName: PageSchema, + modelName: PageModel, + argsType: PageArgs, + engineMiddleware: { + authorization: [AccountTypeEnum.admin], + }, +}); + +const PageBaseMutationAdminResolver = createEngineMutationResolver({ + domain: 'pages', + schemaName: PageSchema, + modelName: PageModel, + newInput: NewPageInput, + editInput: EditPageInput, + engineMiddleware: { + authorization: [AccountTypeEnum.admin], + }, +}); + +@Resolver(PageModel) +export class PageQueryAdminResolver extends PageBaseQueryAdminResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ +} + +@Resolver(PageModel) +export class PageMutationAdminResolver extends PageBaseMutationAdminResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => SuccessResponse) + @EngineMiddleware({ + authorization: [AccountTypeEnum.admin], + }) + async refreshCaching(@Ctx() ctx: ApolloContext): Promise { + const model = new PageModel(); + await updatePagesCaching(model); + + return { success: true }; + } +} + +export const PageAdminResolvers = [PageQueryAdminResolver, PageMutationAdminResolver]; diff --git a/services/module-cms/functions/pages/schemas/pages.input.ts b/services/module-cms/functions/pages/schemas/pages.input.ts new file mode 100644 index 0000000..aa6414b --- /dev/null +++ b/services/module-cms/functions/pages/schemas/pages.input.ts @@ -0,0 +1,16 @@ +import { InputType, ArgsType, Field } from 'type-graphql'; + +import { GetManyArgs } from '@seed/graphql/Request'; +import { PageBaseSchema } from './pages.schema'; + +@InputType() +export class NewPageInput extends PageBaseSchema {} + +@InputType() +export class EditPageInput extends PageBaseSchema { + @Field({ nullable: true }) + title: string; +} + +@ArgsType() +export class PageArgs extends GetManyArgs {} diff --git a/services/module-cms/functions/pages/schemas/pages.schema.ts b/services/module-cms/functions/pages/schemas/pages.schema.ts new file mode 100644 index 0000000..7f33ec3 --- /dev/null +++ b/services/module-cms/functions/pages/schemas/pages.schema.ts @@ -0,0 +1,38 @@ +import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema'; +import { GraphQLJSONObject } from 'graphql-type-json'; +import { InputType, ObjectType, Field, Int } from 'type-graphql'; + +@InputType() +@ObjectType() +export class PageBaseSchema { + @Field() + title: string; + + @Field({ nullable: true }) + label?: string; + + @Field(() => GraphQLJSONObject, { nullable: true }) + content?: any; + + @Field(() => Int, { nullable: true }) + position?: number; + + @Field({ nullable: true }) + toComplete?: boolean; +} + +@ObjectType() +export class PageDBInterfaceSchema extends PageBaseSchema {} + +@ObjectType() +export class PageDBSchema extends PageDBInterfaceSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class PageSchema extends PageDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/services/module-cms/functions/project-settings/project-settings.components.ts b/services/module-cms/functions/project-settings/project-settings.components.ts new file mode 100644 index 0000000..e69de29 diff --git a/services/module-cms/functions/project-settings/project-settings.model.ts b/services/module-cms/functions/project-settings/project-settings.model.ts new file mode 100644 index 0000000..2dc0da6 --- /dev/null +++ b/services/module-cms/functions/project-settings/project-settings.model.ts @@ -0,0 +1,71 @@ +import { ObjectType, Field, ID, ArgsType, InputType } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetManyArgs } from '@seed/graphql/Request'; + +import { SettingsType } from '@seed/interfaces/components'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class ProjectSettingModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'settings', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field(() => SettingsType) + type = SettingsType.env; + + @Field() + key: string; + + @Field() + value: string; + + searchOptions(): string[] { + return ['key', 'type']; + } + filterOptions(): string[] { + return ['key', 'type']; + } + + onCreate(): void { + return; + } +} + +@InputType() +export class NewProjectSettingsInput { + @Field() + key: string; + + @Field() + value: string; +} + +@InputType() +export class EditProjectSettingsInput { + @Field({ nullable: true }) + value: string; +} + +@ArgsType() +export class ProjectSettingsArgs extends GetManyArgs { + @Field(() => SettingsType) + type = SettingsType.env; +} diff --git a/services/module-cms/functions/project-settings/project-settings.resolver.admin.ts b/services/module-cms/functions/project-settings/project-settings.resolver.admin.ts new file mode 100644 index 0000000..fab13d0 --- /dev/null +++ b/services/module-cms/functions/project-settings/project-settings.resolver.admin.ts @@ -0,0 +1,35 @@ +import { Resolver, Query, Args, Authorized, FieldResolver, Root, Ctx } from 'type-graphql'; +import ProjectSettingssModel, { EditProjectSettingsInput, NewProjectSettingsInput, ProjectSettingsArgs } from './project-settings.model'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { GetManyArgs } from '@seed/graphql/Request'; +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +import ProjectSettingModel from './project-settings.model'; + +const ProjectSettingsBaseResolver = createBaseResolver( + 'projectSettings', + ProjectSettingModel, + ProjectSettingsArgs, + NewProjectSettingsInput, + EditProjectSettingsInput, + [AccountTypeEnum.admin], +); + +@Resolver(ProjectSettingssModel) +export default class ProjectSettingsAdminResolver extends ProjectSettingsBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-cms/import/pages.countries.json b/services/module-cms/import/pages.countries.json new file mode 100644 index 0000000..d8e6c77 --- /dev/null +++ b/services/module-cms/import/pages.countries.json @@ -0,0 +1,12690 @@ +{ + "_id" : "011ddfb4-1c00-4123-8d8e-de446c41424d", + "content" : { + "_NEW" : { + "type" : "Array", + "position" : NumberInt(1), + "key" : "countries2", + "locked" : false, + "lockedValue" : false, + "open" : false, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "countries" : { + "label" : "countries", + "position" : NumberInt(2), + "type" : "Array", + "index" : NumberInt(1), + "content" : [ + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Afghanistan", + "fr" : "Afghanistan", + "nl" : "Afghanistan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Åland Islands", + "fr" : "Åland Islands", + "nl" : "Åland Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AX" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Albania", + "fr" : "Albania", + "nl" : "Albania" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Algeria", + "fr" : "Algeria", + "nl" : "Algeria" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "DZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "American Samoa", + "fr" : "American Samoa", + "nl" : "American Samoa" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Andorra", + "fr" : "Andorra", + "nl" : "Andorra" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AD" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Angola", + "fr" : "Angola", + "nl" : "Angola" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Anguilla", + "fr" : "Anguilla", + "nl" : "Anguilla" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Antarctica", + "fr" : "Antarctica", + "nl" : "Antarctica" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AQ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Antigua and Barbuda", + "fr" : "Antigua and Barbuda", + "nl" : "Antigua and Barbuda" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Argentina", + "fr" : "Argentina", + "nl" : "Argentina" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Armenia", + "fr" : "Armenia", + "nl" : "Armenia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Aruba", + "fr" : "Aruba", + "nl" : "Aruba" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Australia", + "fr" : "Australia", + "nl" : "Australia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Austria", + "fr" : "Austria", + "nl" : "Austria" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Azerbaijan", + "fr" : "Azerbaijan", + "nl" : "Azerbaijan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bahamas", + "fr" : "Bahamas", + "nl" : "Bahamas" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bahrain", + "fr" : "Bahrain", + "nl" : "Bahrain" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bangladesh", + "fr" : "Bangladesh", + "nl" : "Bangladesh" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BD" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Barbados", + "fr" : "Barbados", + "nl" : "Barbados" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BB" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Belarus", + "fr" : "Belarus", + "nl" : "Belarus" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Belgium", + "fr" : "Belgium", + "nl" : "Belgium" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Belize", + "fr" : "Belize", + "nl" : "Belize" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Benin", + "fr" : "Benin", + "nl" : "Benin" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BJ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bermuda", + "fr" : "Bermuda", + "nl" : "Bermuda" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bhutan", + "fr" : "Bhutan", + "nl" : "Bhutan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bolivia", + "fr" : "Bolivia", + "nl" : "Bolivia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bosnia and Herzegovina", + "fr" : "Bosnia and Herzegovina", + "nl" : "Bosnia and Herzegovina" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Botswana", + "fr" : "Botswana", + "nl" : "Botswana" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bouvet Island", + "fr" : "Bouvet Island", + "nl" : "Bouvet Island" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BV" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Brazil", + "fr" : "Brazil", + "nl" : "Brazil" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "British Indian Ocean Territory", + "fr" : "British Indian Ocean Territory", + "nl" : "British Indian Ocean Territory" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Brunei Darussalam", + "fr" : "Brunei Darussalam", + "nl" : "Brunei Darussalam" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Bulgaria", + "fr" : "Bulgaria", + "nl" : "Bulgaria" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Burkina Faso", + "fr" : "Burkina Faso", + "nl" : "Burkina Faso" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Burundi", + "fr" : "Burundi", + "nl" : "Burundi" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "BI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cambodia", + "fr" : "Cambodia", + "nl" : "Cambodia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cameroon", + "fr" : "Cameroon", + "nl" : "Cameroon" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Canada", + "fr" : "Canada", + "nl" : "Canada" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cape Verde", + "fr" : "Cape Verde", + "nl" : "Cape Verde" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CV" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cayman Islands", + "fr" : "Cayman Islands", + "nl" : "Cayman Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Central African Republic", + "fr" : "Central African Republic", + "nl" : "Central African Republic" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Chad", + "fr" : "Chad", + "nl" : "Chad" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TD" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Chile", + "fr" : "Chile", + "nl" : "Chile" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "China", + "fr" : "China", + "nl" : "China" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Christmas Island", + "fr" : "Christmas Island", + "nl" : "Christmas Island" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CX" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cocos (Keeling) Islands", + "fr" : "Cocos (Keeling) Islands", + "nl" : "Cocos (Keeling) Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Colombia", + "fr" : "Colombia", + "nl" : "Colombia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Comoros", + "fr" : "Comoros", + "nl" : "Comoros" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Congo", + "fr" : "Congo", + "nl" : "Congo" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Congo, The Democratic Republic of the", + "fr" : "Congo, The Democratic Republic of the", + "nl" : "Congo, The Democratic Republic of the" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CD" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cook Islands", + "fr" : "Cook Islands", + "nl" : "Cook Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Costa Rica", + "fr" : "Costa Rica", + "nl" : "Costa Rica" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cote D'Ivoire", + "fr" : "Cote D'Ivoire", + "nl" : "Cote D'Ivoire" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Croatia", + "fr" : "Croatia", + "nl" : "Croatia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "HR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cuba", + "fr" : "Cuba", + "nl" : "Cuba" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Cyprus", + "fr" : "Cyprus", + "nl" : "Cyprus" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Czech Republic", + "fr" : "Czech Republic", + "nl" : "Czech Republic" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Denmark", + "fr" : "Denmark", + "nl" : "Denmark" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "DK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Djibouti", + "fr" : "Djibouti", + "nl" : "Djibouti" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "DJ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Dominica", + "fr" : "Dominica", + "nl" : "Dominica" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "DM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Dominican Republic", + "fr" : "Dominican Republic", + "nl" : "Dominican Republic" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "DO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Ecuador", + "fr" : "Ecuador", + "nl" : "Ecuador" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "EC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Egypt", + "fr" : "Egypt", + "nl" : "Egypt" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "EG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "El Salvador", + "fr" : "El Salvador", + "nl" : "El Salvador" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SV" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Equatorial Guinea", + "fr" : "Equatorial Guinea", + "nl" : "Equatorial Guinea" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GQ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Eritrea", + "fr" : "Eritrea", + "nl" : "Eritrea" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ER" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Estonia", + "fr" : "Estonia", + "nl" : "Estonia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "EE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Ethiopia", + "fr" : "Ethiopia", + "nl" : "Ethiopia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ET" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Falkland Islands (Malvinas)", + "fr" : "Falkland Islands (Malvinas)", + "nl" : "Falkland Islands (Malvinas)" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "FK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Faroe Islands", + "fr" : "Faroe Islands", + "nl" : "Faroe Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "FO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Fiji", + "fr" : "Fiji", + "nl" : "Fiji" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "FJ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Finland", + "fr" : "Finland", + "nl" : "Finland" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "FI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "France", + "fr" : "France", + "nl" : "France" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "FR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "French Guiana", + "fr" : "French Guiana", + "nl" : "French Guiana" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "French Polynesia", + "fr" : "French Polynesia", + "nl" : "French Polynesia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "French Southern Territories", + "fr" : "French Southern Territories", + "nl" : "French Southern Territories" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Gabon", + "fr" : "Gabon", + "nl" : "Gabon" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Gambia", + "fr" : "Gambia", + "nl" : "Gambia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Georgia", + "fr" : "Georgia", + "nl" : "Georgia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Germany", + "fr" : "Germany", + "nl" : "Germany" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "DE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Ghana", + "fr" : "Ghana", + "nl" : "Ghana" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Gibraltar", + "fr" : "Gibraltar", + "nl" : "Gibraltar" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Greece", + "fr" : "Greece", + "nl" : "Greece" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Greenland", + "fr" : "Greenland", + "nl" : "Greenland" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Grenada", + "fr" : "Grenada", + "nl" : "Grenada" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GD" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Guadeloupe", + "fr" : "Guadeloupe", + "nl" : "Guadeloupe" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GP" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Guam", + "fr" : "Guam", + "nl" : "Guam" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Guatemala", + "fr" : "Guatemala", + "nl" : "Guatemala" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Guernsey", + "fr" : "Guernsey", + "nl" : "Guernsey" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Guinea", + "fr" : "Guinea", + "nl" : "Guinea" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Guinea-Bissau", + "fr" : "Guinea-Bissau", + "nl" : "Guinea-Bissau" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Guyana", + "fr" : "Guyana", + "nl" : "Guyana" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Haiti", + "fr" : "Haiti", + "nl" : "Haiti" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "HT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Heard Island and Mcdonald Islands", + "fr" : "Heard Island and Mcdonald Islands", + "nl" : "Heard Island and Mcdonald Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "HM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Holy See (Vatican City State)", + "fr" : "Holy See (Vatican City State)", + "nl" : "Holy See (Vatican City State)" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "VA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Honduras", + "fr" : "Honduras", + "nl" : "Honduras" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "HN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Hong Kong", + "fr" : "Hong Kong", + "nl" : "Hong Kong" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "HK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Hungary", + "fr" : "Hungary", + "nl" : "Hungary" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "HU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Iceland", + "fr" : "Iceland", + "nl" : "Iceland" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "India", + "fr" : "India", + "nl" : "India" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Indonesia", + "fr" : "Indonesia", + "nl" : "Indonesia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ID" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Iran, Islamic Republic Of", + "fr" : "Iran, Islamic Republic Of", + "nl" : "Iran, Islamic Republic Of" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Iraq", + "fr" : "Iraq", + "nl" : "Iraq" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IQ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Ireland", + "fr" : "Ireland", + "nl" : "Ireland" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Isle of Man", + "fr" : "Isle of Man", + "nl" : "Isle of Man" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Israel", + "fr" : "Israel", + "nl" : "Israel" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Italy", + "fr" : "Italy", + "nl" : "Italy" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "IT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Jamaica", + "fr" : "Jamaica", + "nl" : "Jamaica" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "JM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Japan", + "fr" : "Japan", + "nl" : "Japan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "JP" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Jersey", + "fr" : "Jersey", + "nl" : "Jersey" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "JE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Jordan", + "fr" : "Jordan", + "nl" : "Jordan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "JO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Kazakhstan", + "fr" : "Kazakhstan", + "nl" : "Kazakhstan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Kenya", + "fr" : "Kenya", + "nl" : "Kenya" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Kiribati", + "fr" : "Kiribati", + "nl" : "Kiribati" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Korea, Democratic People'S Republic of", + "fr" : "Korea, Democratic People'S Republic of", + "nl" : "Korea, Democratic People'S Republic of" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KP" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Korea, Republic of", + "fr" : "Korea, Republic of", + "nl" : "Korea, Republic of" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Kuwait", + "fr" : "Kuwait", + "nl" : "Kuwait" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Kyrgyzstan", + "fr" : "Kyrgyzstan", + "nl" : "Kyrgyzstan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Lao People'S Democratic Republic", + "fr" : "Lao People'S Democratic Republic", + "nl" : "Lao People'S Democratic Republic" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Latvia", + "fr" : "Latvia", + "nl" : "Latvia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LV" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Lebanon", + "fr" : "Lebanon", + "nl" : "Lebanon" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LB" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Lesotho", + "fr" : "Lesotho", + "nl" : "Lesotho" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Liberia", + "fr" : "Liberia", + "nl" : "Liberia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Libyan Arab Jamahiriya", + "fr" : "Libyan Arab Jamahiriya", + "nl" : "Libyan Arab Jamahiriya" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Liechtenstein", + "fr" : "Liechtenstein", + "nl" : "Liechtenstein" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Lithuania", + "fr" : "Lithuania", + "nl" : "Lithuania" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Luxembourg", + "fr" : "Luxembourg", + "nl" : "Luxembourg" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Macao", + "fr" : "Macao", + "nl" : "Macao" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Macedonia, The Former Yugoslav Republic of", + "fr" : "Macedonia, The Former Yugoslav Republic of", + "nl" : "Macedonia, The Former Yugoslav Republic of" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Madagascar", + "fr" : "Madagascar", + "nl" : "Madagascar" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Malawi", + "fr" : "Malawi", + "nl" : "Malawi" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Malaysia", + "fr" : "Malaysia", + "nl" : "Malaysia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Maldives", + "fr" : "Maldives", + "nl" : "Maldives" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MV" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Mali", + "fr" : "Mali", + "nl" : "Mali" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ML" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Malta", + "fr" : "Malta", + "nl" : "Malta" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Marshall Islands", + "fr" : "Marshall Islands", + "nl" : "Marshall Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Martinique", + "fr" : "Martinique", + "nl" : "Martinique" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MQ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Mauritania", + "fr" : "Mauritania", + "nl" : "Mauritania" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Mauritius", + "fr" : "Mauritius", + "nl" : "Mauritius" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Mayotte", + "fr" : "Mayotte", + "nl" : "Mayotte" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "YT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Mexico", + "fr" : "Mexico", + "nl" : "Mexico" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MX" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Micronesia, Federated States of", + "fr" : "Micronesia, Federated States of", + "nl" : "Micronesia, Federated States of" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "FM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Moldova, Republic of", + "fr" : "Moldova, Republic of", + "nl" : "Moldova, Republic of" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MD" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Monaco", + "fr" : "Monaco", + "nl" : "Monaco" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Mongolia", + "fr" : "Mongolia", + "nl" : "Mongolia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Montserrat", + "fr" : "Montserrat", + "nl" : "Montserrat" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Morocco", + "fr" : "Morocco", + "nl" : "Morocco" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Mozambique", + "fr" : "Mozambique", + "nl" : "Mozambique" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Myanmar", + "fr" : "Myanmar", + "nl" : "Myanmar" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Namibia", + "fr" : "Namibia", + "nl" : "Namibia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Nauru", + "fr" : "Nauru", + "nl" : "Nauru" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Nepal", + "fr" : "Nepal", + "nl" : "Nepal" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NP" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Netherlands", + "fr" : "Netherlands", + "nl" : "Netherlands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Netherlands Antilles", + "fr" : "Netherlands Antilles", + "nl" : "Netherlands Antilles" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "New Caledonia", + "fr" : "New Caledonia", + "nl" : "New Caledonia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "New Zealand", + "fr" : "New Zealand", + "nl" : "New Zealand" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Nicaragua", + "fr" : "Nicaragua", + "nl" : "Nicaragua" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Niger", + "fr" : "Niger", + "nl" : "Niger" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Nigeria", + "fr" : "Nigeria", + "nl" : "Nigeria" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Niue", + "fr" : "Niue", + "nl" : "Niue" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Norfolk Island", + "fr" : "Norfolk Island", + "nl" : "Norfolk Island" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Northern Mariana Islands", + "fr" : "Northern Mariana Islands", + "nl" : "Northern Mariana Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "MP" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Norway", + "fr" : "Norway", + "nl" : "Norway" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "NO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Oman", + "fr" : "Oman", + "nl" : "Oman" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "OM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Pakistan", + "fr" : "Pakistan", + "nl" : "Pakistan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Palau", + "fr" : "Palau", + "nl" : "Palau" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Palestinian Territory, Occupied", + "fr" : "Palestinian Territory, Occupied", + "nl" : "Palestinian Territory, Occupied" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Panama", + "fr" : "Panama", + "nl" : "Panama" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Papua New Guinea", + "fr" : "Papua New Guinea", + "nl" : "Papua New Guinea" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Paraguay", + "fr" : "Paraguay", + "nl" : "Paraguay" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Peru", + "fr" : "Peru", + "nl" : "Peru" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Philippines", + "fr" : "Philippines", + "nl" : "Philippines" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Pitcairn", + "fr" : "Pitcairn", + "nl" : "Pitcairn" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Poland", + "fr" : "Poland", + "nl" : "Poland" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Portugal", + "fr" : "Portugal", + "nl" : "Portugal" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Puerto Rico", + "fr" : "Puerto Rico", + "nl" : "Puerto Rico" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Qatar", + "fr" : "Qatar", + "nl" : "Qatar" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "QA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Reunion", + "fr" : "Reunion", + "nl" : "Reunion" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "RE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Romania", + "fr" : "Romania", + "nl" : "Romania" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "RO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Russian Federation", + "fr" : "Russian Federation", + "nl" : "Russian Federation" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "RU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "RWANDA", + "fr" : "RWANDA", + "nl" : "RWANDA" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "RW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Saint Helena", + "fr" : "Saint Helena", + "nl" : "Saint Helena" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Saint Kitts and Nevis", + "fr" : "Saint Kitts and Nevis", + "nl" : "Saint Kitts and Nevis" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "KN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Saint Lucia", + "fr" : "Saint Lucia", + "nl" : "Saint Lucia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Saint Pierre and Miquelon", + "fr" : "Saint Pierre and Miquelon", + "nl" : "Saint Pierre and Miquelon" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "PM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Saint Vincent and the Grenadines", + "fr" : "Saint Vincent and the Grenadines", + "nl" : "Saint Vincent and the Grenadines" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "VC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Samoa", + "fr" : "Samoa", + "nl" : "Samoa" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "WS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "San Marino", + "fr" : "San Marino", + "nl" : "San Marino" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Sao Tome and Principe", + "fr" : "Sao Tome and Principe", + "nl" : "Sao Tome and Principe" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ST" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Saudi Arabia", + "fr" : "Saudi Arabia", + "nl" : "Saudi Arabia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Senegal", + "fr" : "Senegal", + "nl" : "Senegal" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Serbia and Montenegro", + "fr" : "Serbia and Montenegro", + "nl" : "Serbia and Montenegro" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Seychelles", + "fr" : "Seychelles", + "nl" : "Seychelles" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Sierra Leone", + "fr" : "Sierra Leone", + "nl" : "Sierra Leone" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Singapore", + "fr" : "Singapore", + "nl" : "Singapore" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Slovakia", + "fr" : "Slovakia", + "nl" : "Slovakia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Slovenia", + "fr" : "Slovenia", + "nl" : "Slovenia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Solomon Islands", + "fr" : "Solomon Islands", + "nl" : "Solomon Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SB" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Somalia", + "fr" : "Somalia", + "nl" : "Somalia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "South Africa", + "fr" : "South Africa", + "nl" : "South Africa" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ZA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "South Georgia and the South Sandwich Islands", + "fr" : "South Georgia and the South Sandwich Islands", + "nl" : "South Georgia and the South Sandwich Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GS" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Spain", + "fr" : "Spain", + "nl" : "Spain" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ES" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Sri Lanka", + "fr" : "Sri Lanka", + "nl" : "Sri Lanka" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "LK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Sudan", + "fr" : "Sudan", + "nl" : "Sudan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SD" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Suriname", + "fr" : "Suriname", + "nl" : "Suriname" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Svalbard and Jan Mayen", + "fr" : "Svalbard and Jan Mayen", + "nl" : "Svalbard and Jan Mayen" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SJ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Swaziland", + "fr" : "Swaziland", + "nl" : "Swaziland" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Sweden", + "fr" : "Sweden", + "nl" : "Sweden" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Switzerland", + "fr" : "Switzerland", + "nl" : "Switzerland" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "CH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Syrian Arab Republic", + "fr" : "Syrian Arab Republic", + "nl" : "Syrian Arab Republic" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "SY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Taiwan, Province of China", + "fr" : "Taiwan, Province of China", + "nl" : "Taiwan, Province of China" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Tajikistan", + "fr" : "Tajikistan", + "nl" : "Tajikistan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TJ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Tanzania, United Republic of", + "fr" : "Tanzania, United Republic of", + "nl" : "Tanzania, United Republic of" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Thailand", + "fr" : "Thailand", + "nl" : "Thailand" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Timor-Leste", + "fr" : "Timor-Leste", + "nl" : "Timor-Leste" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TL" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Togo", + "fr" : "Togo", + "nl" : "Togo" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Tokelau", + "fr" : "Tokelau", + "nl" : "Tokelau" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TK" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Tonga", + "fr" : "Tonga", + "nl" : "Tonga" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TO" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Trinidad and Tobago", + "fr" : "Trinidad and Tobago", + "nl" : "Trinidad and Tobago" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TT" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Tunisia", + "fr" : "Tunisia", + "nl" : "Tunisia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Turkey", + "fr" : "Turkey", + "nl" : "Turkey" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TR" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Turkmenistan", + "fr" : "Turkmenistan", + "nl" : "Turkmenistan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Turks and Caicos Islands", + "fr" : "Turks and Caicos Islands", + "nl" : "Turks and Caicos Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TC" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Tuvalu", + "fr" : "Tuvalu", + "nl" : "Tuvalu" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "TV" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Uganda", + "fr" : "Uganda", + "nl" : "Uganda" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "UG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Ukraine", + "fr" : "Ukraine", + "nl" : "Ukraine" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "UA" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "United Arab Emirates", + "fr" : "United Arab Emirates", + "nl" : "United Arab Emirates" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "AE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "United Kingdom", + "fr" : "United Kingdom", + "nl" : "United Kingdom" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "GB" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "United States", + "fr" : "United States", + "nl" : "United States" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "US" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "United States Minor Outlying Islands", + "fr" : "United States Minor Outlying Islands", + "nl" : "United States Minor Outlying Islands" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "UM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Uruguay", + "fr" : "Uruguay", + "nl" : "Uruguay" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "UY" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Uzbekistan", + "fr" : "Uzbekistan", + "nl" : "Uzbekistan" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "UZ" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Vanuatu", + "fr" : "Vanuatu", + "nl" : "Vanuatu" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "VU" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Venezuela", + "fr" : "Venezuela", + "nl" : "Venezuela" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "VE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Viet Nam", + "fr" : "Viet Nam", + "nl" : "Viet Nam" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "VN" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Virgin Islands, British", + "fr" : "Virgin Islands, British", + "nl" : "Virgin Islands, British" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "VG" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Virgin Islands, U.S.", + "fr" : "Virgin Islands, U.S.", + "nl" : "Virgin Islands, U.S." + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "VI" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Wallis and Futuna", + "fr" : "Wallis and Futuna", + "nl" : "Wallis and Futuna" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "WF" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Western Sahara", + "fr" : "Western Sahara", + "nl" : "Western Sahara" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "EH" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Yemen", + "fr" : "Yemen", + "nl" : "Yemen" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "YE" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Zambia", + "fr" : "Zambia", + "nl" : "Zambia" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ZM" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + { + "label" : "BE", + "position" : NumberInt(1), + "type" : "Object", + "index" : NumberInt(0), + "content" : { + "name" : { + "label" : "name", + "position" : NumberInt(1), + "type" : "input", + "index" : NumberInt(0), + "content" : { + "en" : "Zimbabwe", + "fr" : "Zimbabwe", + "nl" : "Zimbabwe" + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "labelMatch" : false, + "valueMatch" : [ + + ] + }, + "code" : { + "label" : "code", + "position" : NumberInt(2), + "type" : "value", + "index" : NumberInt(1), + "content" : { + + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "value" : "ZW" + } + }, + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "value", + "key" : "code" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + } + ], + "toComplete" : false, + "locked" : false, + "lockedValue" : false, + "open" : true, + "_NEW" : { + "type" : "Object", + "key" : "FR" + }, + "labelMatch" : false, + "valueMatch" : [ + + ] + } + }, + "createdAt" : ISODate("2020-08-20T14:10:34.221+0000"), + "d" : [ + "admin", + "L3iCtdd4sogcaRucTWmUI634ij53" + ], + "label" : "Liste des pays", + "r" : [ + "public", + "L3iCtdd4sogcaRucTWmUI634ij53" + ], + "title" : "countryCodes", + "updatedAt" : ISODate("2020-08-20T14:21:55.663+0000"), + "w" : [ + "admin", + "L3iCtdd4sogcaRucTWmUI634ij53" + ], + "position" : null +} \ No newline at end of file diff --git a/services/module-cms/index.ts b/services/module-cms/index.ts new file mode 100644 index 0000000..f82b7b7 --- /dev/null +++ b/services/module-cms/index.ts @@ -0,0 +1,19 @@ +import 'reflect-metadata'; +import CategoryResolver from '@services/module-cms/functions/categories/category.resolver'; +import CategoryAdminResolver from '@services/module-cms/functions/categories/category.resolver.admin'; +import EmailSettingsAdminResolver from '@services/module-cms/functions/emails-settings/emails-settings.resolver'; +import ProjectSettingsAdminResolver from './functions/project-settings/project-settings.resolver.admin'; +import ListResolver from './functions/lists/list.resolver'; +import ListAdminResolver from './functions/lists/list.resolver.admin'; +import { PageAdminResolvers } from './functions/pages/pages.resolver.admin'; +import ErrorSettingsAdminResolver from './functions/error-settings/errors-settings.resolver'; + +export const ApiCMSAppResolvers = [CategoryResolver, ListResolver]; +export const ApiCMSAdminResolvers = [ + ...PageAdminResolvers, + EmailSettingsAdminResolver, + CategoryAdminResolver, + ProjectSettingsAdminResolver, + ListAdminResolver, + ErrorSettingsAdminResolver, +]; diff --git a/services/module-cms/services/CmsService.ts b/services/module-cms/services/CmsService.ts new file mode 100644 index 0000000..5c16fa0 --- /dev/null +++ b/services/module-cms/services/CmsService.ts @@ -0,0 +1,66 @@ +import { FunctionResponse, fError, fSuccess } from '@seed/interfaces/response'; + +import CMS from '@services/module-cms/functions/pages/pages.model'; + +export default class CMSHelper { + private static instance: CMSHelper; + public EMAILS!: any; + + // eslint-disable-next-line @typescript-eslint/no-empty-function + private constructor() {} + + public static async getEmails(emailTitle: string, language: string): Promise> { + if (!this.instance) this.instance = new CMSHelper(); + if (!this.instance.EMAILS) { + const model = new CMS(); + + const page = await (await model.db()).findOne({ title: 'emails' }); + if (!page) return fError('emailsNotInitializedInCMS'); + + this.instance.EMAILS = page; + } + + if (!this.instance.EMAILS.content[emailTitle] || !this.instance.EMAILS.content[emailTitle].content) { + console.error('emailDataTemplate'); + return fError('emailDataNotFound'); + } + + const emailData = this.instance.EMAILS.content[emailTitle].content; + + const result = { + template: emailData.template ? (emailData.template.value ? emailData.template.value : 'default') : 'default', + subject: emailData.subject ? (emailData.subject.content[language] ? emailData.subject.content[language] : '') : '', + // subtitle: emailData.subtitle ? (emailData.subtitle.content[language] ? emailData.subtitle.content[language] : '') : '', + body: emailData.body ? (emailData.body.content[language] ? emailData.body.content[language] : '') : '', + // button: emailData.button ? (emailData.button.content[language] ? emailData.button.content[language] : undefined) : undefined, + // buttonLink: emailData.buttonLink + // ? emailData.buttonLink.content[language] + // ? emailData.buttonLink.content[language] + // : undefined + // : undefined, + cci: emailData.cci ? (emailData.cci.value ? emailData.cci.value.split(',') : undefined) : undefined, + fromEmail: emailData.fromEmail ? (emailData.fromEmail.value ? emailData.fromEmail.value : '') : '', + replyToEmail: emailData.replyToEmail ? (emailData.replyToEmail.value ? emailData.replyToEmail.value : '') : '', + }; + return fSuccess(result); + } + public static async getEmailTemplate(name = 'default', language = 'en'): Promise> { + if (!this.instance) this.instance = new CMSHelper(); + if (!this.instance.EMAILS) { + const model = new CMS(); + + const page = await (await model.db()).findOne({ title: 'emails' }); + if (!page) return fError('emailsNotInitializedInCMS'); + + this.instance.EMAILS = page; + } + + try { + const template = this.instance.EMAILS.content.template.content[name].content[language]; + return fSuccess(template); + } catch (error) { + console.error('emailTemplateNotFound'); + return fError('emailTemplateNotFound'); + } + } +} diff --git a/services/module-cms/services/PageService.ts b/services/module-cms/services/PageService.ts new file mode 100644 index 0000000..8661060 --- /dev/null +++ b/services/module-cms/services/PageService.ts @@ -0,0 +1,32 @@ +import { promiseAll } from '@seed/helpers/Utils'; +import { uploadToS3 } from '@services/api-uploads/services/S3Service'; +import { AvailableTranslation, TranslatableComponent } from '@src/__components/components'; +import TreatmentContent from './TreatmentContent'; + +export const updateCaching = async (model: any) => { + console.log('Getting all the pages'); + const pages = await model.getAll({ query: {} }); + + const languages = Object.keys(AvailableTranslation); + + const promises: any[] = []; + + const treatment = new TreatmentContent(pages, { languages }); + const contentTest = treatment.getAllTreatments(); + + for (const key in contentTest) { + const contentLanguage = contentTest[key]; + promises.push( + uploadToS3({ + acl: false, + contentType: 'application/json', + buffer: Buffer.from(JSON.stringify(contentLanguage)), + fileName: `content/${process.env.NODE_ENV}-${key}.json`, + }), + ); + } + + await promiseAll(promises); + + console.log('Done uploading to S3'); +}; diff --git a/services/module-cms/services/TreatmentContent.ts b/services/module-cms/services/TreatmentContent.ts new file mode 100644 index 0000000..eff2fb2 --- /dev/null +++ b/services/module-cms/services/TreatmentContent.ts @@ -0,0 +1,172 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import cloneDeep from 'lodash/cloneDeep'; + +interface OptionProps { + languages: string[]; + whitelistedContentKeys?: string[]; + whitelistedCmsPages?: string[]; + blacklistedCmsPages?: string[]; + keyNewContent?: string; + arrayAsObject?: boolean; +} + +class TreatmentContent { + pages: any[]; + + options: any = { + languages: ['en', 'fr', 'nl'], + whitelistedContentKeys: ['position', 'label', 'link', 'url', 'value', 'type', 'src'], + keyNewContent: '_NEW', + arrayAsObject: true, + }; + + constructor(pages: any[], options?: OptionProps) { + // make sure to have json + for (let i = 0; i < pages.length; i += 1) { + const item = pages[i]; + item.content = typeof item.content === 'string' ? JSON.parse(item.content) : item.content; + } + this.pages = pages; + + this.options = { + ...this.options, + ...options, + }; + } + + treatmentContent = (content: object, parentContent: any, parentType: string | undefined, lg?: string): void => { + const keys = Object.keys(content); + keys.forEach((key): void => { + if (key === this.options.keyNewContent) return; + + const elem = content[key]; + const currentContent = elem.content || elem; + + if (!currentContent) return; + + if (elem.type === 'Object') { + // Mode where we keep list as array + if (!this.options.arrayAsObject && parentType && parentType === 'Array') { + const newObj = {}; + parentContent.push(newObj); + this.treatmentContent(currentContent, newObj, undefined, lg); + } else { + let label = key; + // Mode where list are handled as object + if (parentType && parentType === 'Array' && elem.label) { + label = elem.label; + } + parentContent[label] = {}; + if (elem.position) { + parentContent[label].position = parseInt(elem.position, 10); + } + this.treatmentContent(currentContent, parentContent[label], undefined, lg); + } + } else if (elem.type === 'Array') { + if (!this.options.arrayAsObject) { + parentContent[key] = []; + } else { + parentContent[key] = {}; + } + + this.treatmentContent(currentContent, parentContent[key], 'Array', lg); + } else if (elem.type === 'img') { + parentContent[key] = { + src: elem.src, + }; + } else { + const newObj = parentContent[key] ? cloneDeep(parentContent[key]) : {}; + + const subKeys = Object.keys(elem); + subKeys.forEach((subKey): void => { + if (['content', 'alt', 'label'].indexOf(subKey) < 0) { + if (this.options.whitelistedContentKeys.indexOf(subKey) < 0) return; + + newObj[subKey] = elem[subKey]; + // parse number + if (elem[subKey] && typeof elem[subKey] === 'number') { + newObj[subKey] = parseInt(elem[subKey], 10); + } + } else if (subKey === 'label') { + if (parentType === 'Array' && !newObj.value) { + newObj.value = elem[subKey]; + } + if (this.options.whitelistedContentKeys.indexOf(subKey) < 0) return; + + newObj[subKey] = elem[subKey]; + } + }); + // attach content and alt (if present) + const langKeys = Object.keys(currentContent); + langKeys.forEach((langKey): void => { + if (lg && langKey.indexOf(lg) < 0) return; + + newObj[langKey] = currentContent[langKey]; + }); + // attach alt object to image + if (elem.alt) { + newObj.alt = {}; + const altKeys = Object.keys(currentContent); + altKeys.forEach((altKey): void => { + if (lg && altKey.indexOf(lg) < 0) return; + + newObj[altKey] = currentContent[altKey]; + }); + } + + // Attaching new content to the parent + if (parentType && parentType === 'Array') { + if (!this.options.arrayAsObject) { + parentContent.push(newObj); + } else { + const label = newObj.label || key; + parentContent[label] = newObj; + } + } else { + parentContent[newObj.dataKey || key] = newObj; + } + } + }); + }; + + treatmentPage = (page: any, language?: string) => { + const newContent = {}; + + this.treatmentContent(page, newContent, undefined, language); + + return newContent; + }; + + public treatmentPages = (targetLg?: string) => { + const output = {}; + + for (let i = 0; i < this.pages.length; i += 1) { + const item = this.pages[i]; + + const key = item.key || item.title || item.label; + + if (this.options.whitelistedCmsPages && this.options.whitelistedCmsPages.indexOf(key) < 0) continue; + + if (this.options.blacklistedCmsPages && this.options.blacklistedCmsPages.indexOf(key) >= 0) continue; + + if (item.content) output[key] = this.treatmentPage(item.content, targetLg); + } + + return output; + }; + + getAllTreatments = () => { + const output: any = {}; + + for (let i = 0; i < this.options.languages.length; i += 1) { + const lg = this.options.languages[i]; + + output[lg] = this.treatmentPages(lg); + } + output.all = this.treatmentPages(); + + return output; + }; +} + +export default TreatmentContent; diff --git a/services/module-comments/.gitignore b/services/module-comments/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/services/module-comments/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/services/module-comments/README.md b/services/module-comments/README.md new file mode 100644 index 0000000..4d632ed --- /dev/null +++ b/services/module-comments/README.md @@ -0,0 +1,14 @@ +# Make-it Comments Service + +[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com) + +![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master) + +### Documentation + +https://docs.google.com/document/d/1Nw-9-QFOXBXZxrxHPCv6vojtwedWN2hVsihQr-NIza4 + +### Todo + +- Commenting Stats +- Likes, etc diff --git a/services/module-comments/__tests/playground.gql b/services/module-comments/__tests/playground.gql new file mode 100644 index 0000000..00d8f67 --- /dev/null +++ b/services/module-comments/__tests/playground.gql @@ -0,0 +1,13 @@ +mutation addOneReview { + addOneReview(input: { + ressourceId:"1111", + ressourceModel:ArticleModel, + note: 1, + comment: "Test mode" + }) { + _id, + comment + note + createdAt + } +} \ No newline at end of file diff --git a/services/module-comments/functions/comments/comment.model.ts b/services/module-comments/functions/comments/comment.model.ts new file mode 100644 index 0000000..3f0c02a --- /dev/null +++ b/services/module-comments/functions/comments/comment.model.ts @@ -0,0 +1,47 @@ +import { ObjectType, Field } from 'type-graphql'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { EngineModel } from '@lib/seed/engine/EngineModel'; +import { plainToClass } from 'class-transformer'; +import { CustomSearchEngine } from '@seed/services/database/DBRequestService'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import { CommentDBIntefaceSchema, CommentDBSchema, CommentSchema } from './schemas/comments.schema'; +import { CommentArgsSchemma } from './schemas/comments.schema.input'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public, AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class CommentModel extends EngineModel { + public constructor(input?: CommentDBIntefaceSchema & Partial) { + const dataInit = plainToClass(CommentDBSchema, input || {}); + super({ + // ...init, + collectionName: 'comments.reviews', + permissions: permissions, + dataInit, + }); + } + + @Field({ nullable: true }) + idk?: string; + + searchEngine(): CustomSearchEngine { + const sEngine: CustomSearchEngine = { + ressourceId: { + operation: '$eq', + }, + }; + + return sEngine; + } + + plainToClass(plain: any): CommentSchema | CommentSchema[] { + return plainToClass(CommentSchema, plain); + } +} diff --git a/services/module-comments/functions/comments/comment.service.ts b/services/module-comments/functions/comments/comment.service.ts new file mode 100644 index 0000000..14ad5cc --- /dev/null +++ b/services/module-comments/functions/comments/comment.service.ts @@ -0,0 +1,111 @@ +// import CommentModel, { NewCommentInput } from './comment.model'; +// import { ApolloContext } from '@seed/interfaces/context'; +// import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +// import { addOneGeneric } from '@seed/graphql/BaseService'; + +import { GetArgs } from '@seed/graphql/Request'; +import { ApolloContext } from '@seed/interfaces/context'; +import { modelsLoaders } from '@src/__indexes/__loaders'; +import CommentModel from './comment.model'; +import { CommentSchema } from './schemas/comments.schema'; +import { CommentNewInputSchema } from './schemas/comments.schema.input'; + +export class CommentService { + static async getComments(ressourceId: string, pagination?: GetArgs): Promise { + try { + return new CommentModel().getMany({ query: { ressourceId }, pagination }); + } catch (error) { + throw error; + } + } + + static async addComment(input: CommentNewInputSchema, sentBy: string): Promise { + try { + // Check if the loader for that ressource exist + let modelGeneric = modelsLoaders[input.ressourceModel]; + if (modelGeneric) { + try { + await modelGeneric.getOne({ query: { _id: input.ressourceId } }); + await modelGeneric.updateOneCustom({ query: { _id: input.ressourceId }, updateRequest: { $inc: { totalComment: 1 } } }); + } catch (error) { + console.error(error); + } + } + + // if (input.parentId) { + // const commentModel = await new VideoCommentModel().getOne({ _id: input.parentId }); + + // // Update count of reply + // await commentModel.updateOne({ _id: commentModel._id }, { totalReplies: (commentModel.totalReplies || 0) + 1 }); + // } + + const newComment = new CommentModel({ + ...input, + sentBy, + }); + + return await newComment.saveOne({}); + } catch (error) { + throw error; + } + } +} + +// @Mutation(() => VideoCommentModel) +// @Authorized() +// async deleteVideoComment(@Arg('input') input: DeleteVideoCommentInput, @Ctx() ctx: ApolloContext): Promise { +// try { +// const comment = await new VideoCommentModel().getOne({ _id: input.commentId }); + +// if (comment.userId != ctx.ctx.user._id) throw new ApolloError('notAllowed', '400'); + +// // Update count of comments +// const videoModel = await new VideoModel().getOne({ _id: comment.videoId }); +// await videoModel.updateOne({ _id: videoModel._id }, { totalComment: (videoModel.totalComment || 1) - 1 }); + +// if (comment.parentId) { +// // Update count of reply +// await comment.updateOne({ _id: comment.parentId }, { totalReplies: (comment.totalReplies || 1) - 1 }); +// } + +// return deleteOneGeneric(comment, input.commentId); +// } catch (error) { +// throw error; +// } + +// @Mutation(() => CommentModel) +// @Authorized() +// async editComment(@Arg('id') id: string, @Arg('input') input: EditCommentInput, @Ctx() ctx: ApolloContext): Promise { +// const model = new CommentModel(); +// const message = await model.getOne({ _id: id }); + +// if (message.sentBy !== ctx.ctx.user._id) +// throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: message.sentBy }); + +// return model.updateOne( +// { _id: id }, +// { +// message: input.message, +// edited: true, +// updatedAt: new Date(), +// }, +// ); +// } + +// @Mutation(() => CommentModel) +// @Authorized() +// async deleteComment(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { +// const model = new CommentModel(); +// const message = await model.getOne({ _id: id }); + +// if (message.sentBy !== ctx.ctx.user._id) +// throw new ApolloError('permissions.notAllowed', '400', { you: ctx.ctx.user._id, allowed: message.sentBy }); + +// return model.updateOne( +// { _id: id }, +// { +// deleted: true, +// updatedAt: new Date(), +// }, +// ); +// } diff --git a/services/module-comments/functions/comments/commentStat.model.ts b/services/module-comments/functions/comments/commentStat.model.ts new file mode 100644 index 0000000..118f1b6 --- /dev/null +++ b/services/module-comments/functions/comments/commentStat.model.ts @@ -0,0 +1,60 @@ +import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import ImageComponent from '@seed/interfaces/components'; +import { GetArgs } from '@seed/graphql/Request'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export class CommentReadBy { + @Field() + accountId: string; + + @Field() + readAt: Date; +} + +@ObjectType() +export default class CommentStatModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'stats', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field() + ressourceId: string; + + @Field() + totalComment: string; + + @Field() + lastCommented: Date; + + searchOptions(): string[] { + return []; + } + filterOptions(): string[] { + return []; + } +} + +@ArgsType() +export class CommentArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} diff --git a/services/module-comments/functions/comments/resolvers/comment.resolver.ts b/services/module-comments/functions/comments/resolvers/comment.resolver.ts new file mode 100644 index 0000000..78dda76 --- /dev/null +++ b/services/module-comments/functions/comments/resolvers/comment.resolver.ts @@ -0,0 +1,53 @@ +import { Resolver } from 'type-graphql'; +import { createEngineMutationResolver, createEngineQueryResolver } from '@seed/engine/genericResolvers/BaseEngineResolver'; + +import { AccountTypeEnum } from '@src/accounts/account.components'; + +import { CommentSchema } from '../schemas/comments.schema'; +import { CommentArgsSchemma, CommentNewInputSchema, CommentEditInputSchema } from '../schemas/comments.schema.input'; +import CommentModel from '../comment.model'; + +const CommentGenericQueryResolver = createEngineQueryResolver({ + domain: 'comments', + schemaName: CommentSchema, + modelName: CommentModel, + argsType: CommentArgsSchemma, + engineMiddleware: {}, +}); + +@Resolver(CommentSchema) +export class CommentQueryResolver extends CommentGenericQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ +} + +const CommentGenericMutationResolver = createEngineMutationResolver({ + domain: 'comments', + schemaName: CommentSchema, + modelName: CommentModel, + newInput: CommentNewInputSchema, + editInput: CommentEditInputSchema, + engineMiddleware: { + authorization: [AccountTypeEnum.user], + }, +}); + +@Resolver(CommentSchema) +export class CommentMutationResolver extends CommentGenericMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} + +export const CommentResolvers = [CommentQueryResolver, CommentMutationResolver]; diff --git a/services/module-comments/functions/comments/schemas/comments.schema.input.ts b/services/module-comments/functions/comments/schemas/comments.schema.input.ts new file mode 100644 index 0000000..06fbdb1 --- /dev/null +++ b/services/module-comments/functions/comments/schemas/comments.schema.input.ts @@ -0,0 +1,34 @@ +import ImageComponent from '@lib/seed/interfaces/components'; +import { ArgsType, Field, InputType, Int } from 'type-graphql'; +import { CommentReviewInputSchema } from '../../components'; +import { CommentSchema } from './comments.schema'; + +@InputType() +export class CommentNewInputSchema extends CommentReviewInputSchema implements Partial { + @Field() + comment: string; +} + +@InputType() +export class CommentNewInputCustomSchema implements Partial { + @Field() + comment: string; + + @Field(() => ImageComponent, { nullable: true }) + file?: ImageComponent; +} + +@InputType() +export class CommentEditInputSchema implements Partial { + @Field({ nullable: true }) + comment?: string; + + @Field(() => ImageComponent, { nullable: true }) + file?: ImageComponent; +} + +@ArgsType() +export class CommentArgsSchemma { + @Field() + ressourceId: string; +} diff --git a/services/module-comments/functions/comments/schemas/comments.schema.ts b/services/module-comments/functions/comments/schemas/comments.schema.ts new file mode 100644 index 0000000..7613515 --- /dev/null +++ b/services/module-comments/functions/comments/schemas/comments.schema.ts @@ -0,0 +1,25 @@ +import { ObjectType, Field, ArgsType, InputType, Int } from 'type-graphql'; + +import { IEngineSchema, MetaBy, MetaPermissions } from '@lib/seed/engine/EngineSchema'; +import { IsNotEmpty } from 'class-validator'; +import { CommentReviewDBBaseSchema } from '../../components'; + +@ObjectType() +export class CommentDBIntefaceSchema extends CommentReviewDBBaseSchema { + @Field() + @IsNotEmpty() + comment: string; +} + +@ObjectType() +export class CommentDBSchema extends CommentDBIntefaceSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class CommentSchema extends CommentDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/services/module-comments/functions/components.ts b/services/module-comments/functions/components.ts new file mode 100644 index 0000000..9c43022 --- /dev/null +++ b/services/module-comments/functions/components.ts @@ -0,0 +1,83 @@ +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +import { IsRefExist } from '@lib/seed/engine/decorators/db.guard'; +import ImageComponent from '@lib/seed/interfaces/components'; +import { CommentModelEnum } from '@src/__components/components'; +import { IsNotEmpty } from 'class-validator'; +import { InputType, ObjectType, Field, Int, UseMiddleware, ArgsType } from 'type-graphql'; + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@InputType() +@ObjectType() +export class ReportedInfo { + @Field() + accountId: string; + + @Field() + addedAt: Date; +} + +@InputType() +@ObjectType() +export class CommentReviewInputSchema { + @Field(() => CommentModelEnum) + @IsNotEmpty() + ressourceModel: CommentModelEnum; + + @Field() + @IsNotEmpty() + @IsRefExist() + ressourceId: string; + + @Field(() => ImageComponent, { nullable: true }) + file?: ImageComponent; +} + +@InputType() +@ObjectType() +export class CommentReviewDBBaseSchema { + @Field(() => Int, { nullable: true }) + note?: number; + + @Field({ nullable: true }) + comment?: string; + + @Field(() => ImageComponent, { nullable: true }) + file?: ImageComponent; + + @Field(() => [ReportedInfo], { nullable: true }) + reported?: ReportedInfo[]; + + @Field() + @IsNotEmpty() + sentBy: string; + + @Field({ nullable: true }) + edited?: boolean; + + @Field({ nullable: true }) + deleted?: boolean; + + @Field({ nullable: true }) + parentId?: string; + + @Field(() => Int, { nullable: true }) + numberReplies?: number; +} diff --git a/services/module-comments/functions/reviews/resolvers/reviews.resolver.ts b/services/module-comments/functions/reviews/resolvers/reviews.resolver.ts new file mode 100644 index 0000000..fbdf6e4 --- /dev/null +++ b/services/module-comments/functions/reviews/resolvers/reviews.resolver.ts @@ -0,0 +1,53 @@ +import { Resolver } from 'type-graphql'; + +import { createEngineMutationResolver, createEngineQueryResolver } from '@seed/engine/genericResolvers/BaseEngineResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; + +import { ReviewSchema } from '../schemas/reviews.schema'; +import ReviewModel from '../reviews.model'; +import { ReviewArgsSchemma, ReviewEditInputSchema, ReviewNewInputSchema } from '../schemas/reviews.schema.input'; + +const ReviewGenericQueryResolver = createEngineQueryResolver({ + domain: 'reviews', + schemaName: ReviewSchema, + modelName: ReviewModel, + argsType: ReviewArgsSchemma, + engineMiddleware: {}, +}); + +@Resolver(ReviewSchema) +export class ReviewQueryResolver extends ReviewGenericQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ +} + +const ReviewGenericMutationResolver = createEngineMutationResolver({ + domain: 'reviews', + schemaName: ReviewSchema, + modelName: ReviewModel, + newInput: ReviewNewInputSchema, + editInput: ReviewEditInputSchema, + engineMiddleware: { + authorization: [AccountTypeEnum.user], + }, +}); + +@Resolver(ReviewSchema) +export class ReviewMutationResolver extends ReviewGenericMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} + +export const ReviewResolvers = [ReviewQueryResolver, ReviewMutationResolver]; diff --git a/services/module-comments/functions/reviews/reviews.model.ts b/services/module-comments/functions/reviews/reviews.model.ts new file mode 100644 index 0000000..81b7de4 --- /dev/null +++ b/services/module-comments/functions/reviews/reviews.model.ts @@ -0,0 +1,52 @@ +import { ObjectType } from 'type-graphql'; + +import { Permission } from '@seed/interfaces/permission'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { EngineModel } from '@lib/seed/engine/EngineModel'; +import { plainToClass } from 'class-transformer'; +import { CustomSearchEngine } from '@seed/services/database/DBRequestService'; +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import { ReviewDBIntefaceSchema, ReviewDBSchema, ReviewSchema } from './schemas/reviews.schema'; +import { ReviewArgsSchemma } from './schemas/reviews.schema.input'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public, AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class ReviewModel extends EngineModel { + public constructor(input?: ReviewDBIntefaceSchema & Partial) { + const dataInit = plainToClass(ReviewDBSchema, input || {}); + super({ + // ...init, + collectionName: 'comments.reviews', + permissions: permissions, + dataInit, + }); + } + + searchEngine(): CustomSearchEngine { + const sEngine: CustomSearchEngine = { + ressourceId: { + operation: '$eq', + }, + noteFilter: { + operation: 'between', + dbFName: 'note', + beetweenConfig: { + inputFromField: 'noteMin', + inputToField: 'noteMax', + }, + }, + }; + + return sEngine; + } + + plainToClass(plain: any): ReviewSchema | ReviewSchema[] { + return plainToClass(ReviewSchema, plain); + } +} diff --git a/services/module-comments/functions/reviews/schemas/reviews.schema.input.ts b/services/module-comments/functions/reviews/schemas/reviews.schema.input.ts new file mode 100644 index 0000000..d18600e --- /dev/null +++ b/services/module-comments/functions/reviews/schemas/reviews.schema.input.ts @@ -0,0 +1,36 @@ +import ImageComponent from '@lib/seed/interfaces/components'; +import { ArgsType, Field, InputType, Int } from 'type-graphql'; +import { CommentReviewInputSchema } from '../../components'; +import { ReviewSchema } from './reviews.schema'; + +@InputType() +export class ReviewNewInputSchema extends CommentReviewInputSchema implements Partial { + @Field(() => Int) + note: number; + + @Field({ nullable: true }) + comment?: string; +} + +@InputType() +export class ReviewEditInputSchema implements Partial { + @Field(() => Int, { nullable: true }) + note?: number; + + @Field({ nullable: true }) + comment?: string; + + @Field(() => ImageComponent, { nullable: true }) + file?: ImageComponent; +} + +@ArgsType() +export class ReviewArgsSchemma { + @Field() + ressourceId: string; + + @Field(() => Int, { nullable: true }) + noteMin?: number; + @Field(() => Int, { nullable: true }) + noteMax?: number; +} diff --git a/services/module-comments/functions/reviews/schemas/reviews.schema.ts b/services/module-comments/functions/reviews/schemas/reviews.schema.ts new file mode 100644 index 0000000..3712f15 --- /dev/null +++ b/services/module-comments/functions/reviews/schemas/reviews.schema.ts @@ -0,0 +1,23 @@ +import { ObjectType, Field, ArgsType, InputType, Int } from 'type-graphql'; + +import { IEngineSchema, MetaBy, MetaPermissions } from '@lib/seed/engine/EngineSchema'; +import { IsNotEmpty } from 'class-validator'; +import { CommentReviewDBBaseSchema } from '../../components'; + +export class ReviewDBIntefaceSchema extends CommentReviewDBBaseSchema { + @Field(() => Int) + @IsNotEmpty() + note: number; +} + +export class ReviewDBSchema extends ReviewDBIntefaceSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class ReviewSchema extends ReviewDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/services/module-favorites/.gitignore b/services/module-favorites/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/services/module-favorites/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/services/module-favorites/README.md b/services/module-favorites/README.md new file mode 100644 index 0000000..3811eaa --- /dev/null +++ b/services/module-favorites/README.md @@ -0,0 +1,34 @@ +# module-favorites + +Need to add this to the account Model + + @Field(() => [FavoriteComponent], { nullable: 'itemsAndList' }) + favLikes?: FavoriteComponent[]; + + +Need to add the corresponding resolvers +-- app + + AccountFavResolver, + AccountLikeResolver + +-- admin + +Need to update the loaders & ressource enum + +export enum RessourceEnum { + 'NAME_OF_RESSOURCE' = 'NAME_OF_RESSOURCE', +} +registerEnumType(RessourceEnum, { + name: 'RessourceEnum', +}); + +export const modelsLoaders = { + accounts: new AccountModel(), + NAME_OF_RESSOURCE: new NAME_OF_RESSOURCE_MODEL(), +}; + + + + + diff --git a/services/module-favorites/__tests/favorites.playground.graphql b/services/module-favorites/__tests/favorites.playground.graphql new file mode 100644 index 0000000..c38d2c5 --- /dev/null +++ b/services/module-favorites/__tests/favorites.playground.graphql @@ -0,0 +1,7 @@ +query me {me{favLikes{ressourceId,ressourceType,addedAt,favorite,liked}}} + +mutation accountsFavoritesAddOne{accountsFavoritesAddOne(input:{ressourceId:"7d9c6ffb-a1c2-4e5d-9488-663725a1c730",ressourceType:products}){ressourceId,ressourceType,addedAt,favorite}} +mutation accountsFavoritesRemoveOne{accountsFavoritesRemoveOne(input:{ressourceId:"7d9c6ffb-a1c2-4e5d-9488-663725a1c730"}){success}} +mutation accountsLikesAddOne{accountsLikesAddOne(input:{ressourceId:"7d9c6ffb-a1c2-4e5d-9488-663725a1c730",ressourceType:products}){ressourceId,ressourceType,addedAt,favorite}} +mutation accountsDislikesAddOne{accountsDislikesAddOne(input:{ressourceId:"7d9c6ffb-a1c2-4e5d-9488-663725a1c730",ressourceType:products}){ressourceId,ressourceType,addedAt,favorite}} +mutation accountsLikesRemoveOne{accountsLikesRemoveOne(input:{ressourceId:"7d9c6ffb-a1c2-4e5d-9488-663725a1c730"}){success}} \ No newline at end of file diff --git a/services/module-favorites/components.ts b/services/module-favorites/components.ts new file mode 100644 index 0000000..6f7abe9 --- /dev/null +++ b/services/module-favorites/components.ts @@ -0,0 +1,54 @@ +import { ObjectType, Field, InputType } from 'type-graphql'; +import { RessourceEnum } from '@src/__components/components'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@ObjectType() +export class FavoriteComponent { + @Field() + ressourceId: string; + + @Field(() => RessourceEnum) + ressourceType: RessourceEnum; + + @Field() + addedAt: Date; + + @Field({ nullable: true }) + favorite?: boolean; + @Field({ nullable: true }) + liked?: boolean; +} + +@InputType() +export class AddInput { + @Field() + ressourceId: string; + + @Field(() => RessourceEnum) + ressourceType: RessourceEnum; +} + +@InputType() +export class RemoveInput { + @Field() + ressourceId: string; +} diff --git a/services/module-favorites/functions/favLikes/favLikes.resolver.admin.ts b/services/module-favorites/functions/favLikes/favLikes.resolver.admin.ts new file mode 100644 index 0000000..f3f8ffe --- /dev/null +++ b/services/module-favorites/functions/favLikes/favLikes.resolver.admin.ts @@ -0,0 +1,14 @@ +import AccountModel from '@src/accounts/account.model'; +import { Resolver } from 'type-graphql'; + +@Resolver(AccountModel) +export default class AccountFavLikeAdminResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-favorites/functions/favLikes/favLikes.resolver.ts b/services/module-favorites/functions/favLikes/favLikes.resolver.ts new file mode 100644 index 0000000..2040cec --- /dev/null +++ b/services/module-favorites/functions/favLikes/favLikes.resolver.ts @@ -0,0 +1,211 @@ +import { Resolver, Arg, Mutation, Ctx, Authorized, FieldResolver, Root } from 'type-graphql'; +import AccountModel from '@src/accounts/account.model'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { AddInput, FavoriteComponent, RemoveInput } from '@services/module-favorites/components'; +import { ApolloError } from 'apollo-server-lambda'; +import { modelsLoaders } from '@src/__indexes/__loaders'; +import { BaseGraphModel } from '@lib/seed/graphql/BaseGraphModel'; +import _ from 'lodash'; +import { SuccessResponse } from '@lib/seed/interfaces/response'; +import { oneToManyComplexity } from '@lib/seed/graphql/Middleware'; + +@Resolver(AccountModel) +export class AccountFavResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + @FieldResolver(() => [FavoriteComponent], { complexity: oneToManyComplexity }) + async getFavorites(@Root() account: AccountModel, @Ctx() ctx: ApolloContext): Promise<(FavoriteComponent | Error)[]> { + const favLikes = account.favLikes || []; + return _.filter(favLikes, { favorite: true }); + } + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => FavoriteComponent) + @Authorized() + async accountsFavoritesAddOne(@Arg('input') input: AddInput, @Ctx() ctx: ApolloContext): Promise { + const currentAccount = ctx.ctx.user; + // Check if element exists + if (!modelsLoaders[input.ressourceType]) throw new ApolloError('badConfiguration', '400'); + + const ressourceModel = modelsLoaders[input.ressourceType]; + await ressourceModel.getOne({ _id: input.ressourceId }, ctx); + + const elementToSave: FavoriteComponent = { + ...input, + favorite: true, + addedAt: new Date(), + }; + + const favLikes: FavoriteComponent[] = currentAccount.favLikes || []; + + // Check if already in the sub array + const index = _.findIndex(favLikes, { ressourceId: input.ressourceId }); + if (index === -1) favLikes.push(elementToSave); + else { + favLikes[index].favorite = true; + } + + await currentAccount.updateOne({ _id: currentAccount._id }, { favLikes: favLikes }, ctx); + + return elementToSave; + } + + @Mutation(() => SuccessResponse) + @Authorized() + async accountsFavoritesRemoveOne(@Arg('input') input: RemoveInput, @Ctx() ctx: ApolloContext): Promise { + const currentAccount = ctx.ctx.user; + const favLikes: FavoriteComponent[] = currentAccount.favLikes || []; + + // Check if already in the sub array + const index = _.findIndex(favLikes, { ressourceId: input.ressourceId }); + if (index === -1) return { success: true }; + else { + // Check needs to remove entirely or just from the field + if (favLikes[index].liked === undefined) favLikes.splice(index, 1); + else favLikes[index].favorite = undefined; + } + + await currentAccount.updateOne({ _id: currentAccount._id }, { favLikes: favLikes }, ctx); + + return { success: true }; + } +} + +@Resolver(AccountModel) +export class AccountLikeResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + @FieldResolver(() => [FavoriteComponent], { complexity: oneToManyComplexity }) + async getLiked(@Root() account: AccountModel, @Ctx() ctx: ApolloContext): Promise<(FavoriteComponent | Error)[]> { + const favLikes = account.favLikes || []; + return _.filter(favLikes, { liked: true }); + } + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => FavoriteComponent) + @Authorized() + async accountsLikesAddOne(@Arg('input') input: AddInput, @Ctx() ctx: ApolloContext): Promise { + const currentAccount = ctx.ctx.user; + // Check if element exists + if (!modelsLoaders[input.ressourceType]) throw new ApolloError('badConfiguration', '400'); + + const ressourceModel = modelsLoaders[input.ressourceType]; + await ressourceModel.getOne({ _id: input.ressourceId }, ctx); + + const elementToSave: FavoriteComponent = { + ...input, + liked: true, + addedAt: new Date(), + }; + + const favLikes: FavoriteComponent[] = currentAccount.favLikes || []; + + // Check if already in the sub array + const index = _.findIndex(favLikes, { ressourceId: input.ressourceId }); + if (index === -1) favLikes.push(elementToSave); + else { + favLikes[index].liked = true; + } + + await currentAccount.updateOne({ _id: currentAccount._id }, { favLikes: favLikes }, ctx); + + return elementToSave; + } + + @Mutation(() => FavoriteComponent) + @Authorized() + async accountsDislikesAddOne(@Arg('input') input: AddInput, @Ctx() ctx: ApolloContext): Promise { + const currentAccount = ctx.ctx.user; + // Check if element exists + if (!modelsLoaders[input.ressourceType]) throw new ApolloError('badConfiguration', '400'); + + const ressourceModel = modelsLoaders[input.ressourceType]; + await ressourceModel.getOne({ _id: input.ressourceId }, ctx); + + const elementToSave: FavoriteComponent = { + ...input, + liked: false, + addedAt: new Date(), + }; + + const favLikes: FavoriteComponent[] = currentAccount.favLikes || []; + + // Check if already in the sub array + const index = _.findIndex(favLikes, { ressourceId: input.ressourceId }); + if (index === -1) favLikes.push(elementToSave); + else { + favLikes[index].liked = false; + } + + await currentAccount.updateOne({ _id: currentAccount._id }, { favLikes: favLikes }, ctx); + + return elementToSave; + } + + @Mutation(() => SuccessResponse) + @Authorized() + async accountsLikesRemoveOne(@Arg('input') input: RemoveInput, @Ctx() ctx: ApolloContext): Promise { + const currentAccount = ctx.ctx.user; + const favLikes: FavoriteComponent[] = currentAccount.favLikes || []; + + // Check if already in the sub array + const index = _.findIndex(favLikes, { ressourceId: input.ressourceId }); + if (index === -1) return { success: true }; + else { + // Check needs to remove entirely or just from the field + if (favLikes[index].favorite === undefined) favLikes.splice(index, 1); + else favLikes[index].liked = undefined; + } + + await currentAccount.updateOne({ _id: currentAccount._id }, { favLikes: favLikes }, ctx); + + return { success: true }; + } +} diff --git a/services/module-payments/.gitignore b/services/module-payments/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/services/module-payments/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/services/module-payments/README.md b/services/module-payments/README.md new file mode 100644 index 0000000..717d94a --- /dev/null +++ b/services/module-payments/README.md @@ -0,0 +1,28 @@ +# module-payments + +Need to add this to the account Model + + @Field(() => AccountPaymentInfo, { nullable: true }) + paymentInfo?: AccountPaymentInfo; + +Need to add the corresponding resolvers +-- app + + ...ModulePaymentAppResolver + +-- admin + +Stripe Install + + - Install the env in DB + +Stripe config + +Url to add in the stripe webhook + +-- Custom domain (recommended) +https://xxx-api.example.com/payments/webhook + +Once you have setup a webhook, you will received a signature key whsec_XXXX that you need to put as STRIPE_HOOK_SECRET in your env + + diff --git a/services/module-payments/__examples/checkout.resolver.ts b/services/module-payments/__examples/checkout.resolver.ts new file mode 100644 index 0000000..0817d7b --- /dev/null +++ b/services/module-payments/__examples/checkout.resolver.ts @@ -0,0 +1,46 @@ +import { Resolver, Authorized, Ctx } from 'type-graphql'; +import { Mutation, Arg } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; + +// From Module +import { OrderEngineSchema } from '../functions/orders/schemas/order.schema'; +import { CheckoutEngineInput } from '../functions/orders/schemas/order.schema.input'; + +// From SRC + +// EXAMPLES + +// @Resolver(OrderModel) +// export default class CheckoutEngineResolver { +// /* +// ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ +// ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ +// ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ +// ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ +// ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ +// ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ +// */ + +// /* +// ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ +// ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ +// ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ +// ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ +// ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ +// ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ +// */ + +// async cartsCheckout( +// @Arg('input') input: CheckoutEngineInput, +// @Arg('paymentType', { nullable: true }) paymentType: PaymentProvider, +// @Ctx() ctx: ApolloContext, +// ): Promise { +// try { +// const pt = paymentType || 'stripe'; + +// return await createCheckout({ payment: pt, contactInfo: input, input, ctx }); +// } catch (error) { +// throw error; +// } +// } +// } diff --git a/services/module-payments/__examples/env.import.json b/services/module-payments/__examples/env.import.json new file mode 100644 index 0000000..3ee62fa --- /dev/null +++ b/services/module-payments/__examples/env.import.json @@ -0,0 +1,102 @@ +{ + "_id" : "8c4c28fb-d89d-434c-a962-f4c43dbda68d", + "key" : "STRIPE_SKEY", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "dec94533-a457-4c66-9ead-4e754243ec89", + "key" : "STRIPE_PUBLIC_KEY", + "value" :"", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "870d1674-367a-4937-9d66-d36e889d4953", + "key" : "STRIPE_CURRENCY", + "value" :"usd", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "5ed19459-1b4b-462b-88fb-5fbb2d698229", + "key" : "STRIPE_ORDERDESCRIPTION", + "value" :"You order from our webplatform", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "5ed19459-1b4b-462b-88fb-5fxx2d698229", + "key" : "STRIPE_ACCOUNT_REFRESH_URL", + "value" :"https://makeit-studio.com", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, +{ + "_id" : "5edxx459-1b4b-462b-88fb-5fxx2d698229", + "key" : "STRIPE_ACCOUNT_RETURN_URL", + "value" :"https://makeit-studio.com", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin", + ], + "w" : [ + "admin", + ], + "d" : [ + "admin", + ], +}, \ No newline at end of file diff --git a/services/module-payments/__examples/stripe.env.json b/services/module-payments/__examples/stripe.env.json new file mode 100644 index 0000000..ead0408 --- /dev/null +++ b/services/module-payments/__examples/stripe.env.json @@ -0,0 +1,85 @@ +{ + "_id" : "5ed19459-1b4b-462b-88fb-5fbb2d698229", + "key" : "STRIPE_ORDERDESCRIPTION", + "value" : "order from our webplatform", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ] +} +{ + "_id" : "870d1674-367a-4937-9d66-d36e889d4953", + "key" : "STRIPE_CURRENCY", + "value" : "", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ] +} +{ + "_id" : "dec94533-a457-4c66-9ead-4e754243ec89", + "key" : "STRIPE_PUBLIC_KEY", + "value" : "", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ] +} +{ + "_id" : "8c4c28fb-d89d-434c-a962-f4c43dbda68d", + "key" : "STRIPE_SKEY", + "value" : "", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ] +} +{ + "_id" : "8c4c28fb-xx-434c-xx-f4c43dbda68d", + "key" : "STRIPE_HOOK_SECRET", + "value" : "", + "type" : "env", + "createdAt" : ISODate(), + "updatedAt" : ISODate(), + "r" : [ + "admin" + ], + "w" : [ + "admin" + ], + "d" : [ + "admin" + ] +} \ No newline at end of file diff --git a/services/module-payments/__handlers/stripeHookHandler.ts b/services/module-payments/__handlers/stripeHookHandler.ts new file mode 100644 index 0000000..f56fe60 --- /dev/null +++ b/services/module-payments/__handlers/stripeHookHandler.ts @@ -0,0 +1,102 @@ +import 'reflect-metadata'; +import axios from 'axios'; +import serverless from 'serverless-http'; +import express from 'express'; + +import { OrderEngineDBSchema } from '@services/module-payments/functions/orders/schemas/order.schema'; +import { SettingsCache } from '@lib/seed/graphql/Settings'; + +import StripeService from '@services/module-payments/services/stripe/StripeService'; +import { OrderStatusEnum } from '@services/module-payments/components/components.orders'; +import { PaymentStatusEnum } from '@services/module-payments/components/components.payments'; +import DB from '@seed/services/database/DBService'; + +import {afterPaymentSuccess, afterPaymentFailed } from '@src/orders/services/order.service' + + +const app = express(); + +app.use(require('body-parser').text({ type: '*/*' })); +app.post('/payments/webhook', async function (request, response) { + const settingsI = SettingsCache.getInstance(); + await settingsI.refreshCache(); + + + const body = request.body; + const signature = request.headers['stripe-signature']; + + const endpointSecret = process.env.STRIPE_HOOK_SECRET || ''; + + let event; + + const stripeInstance = await StripeService.getInstance(); + + try { + event = stripeInstance.stripe.webhooks.constructEvent(body, signature as any, endpointSecret); + } catch (err) { + console.error('[STRIPE] Hooks - ERROR', err); + response.status(400).end(); + return; + } + + const intent = event.data.object; + console.log('[STRIPE] Hooks - event', event['type'], 'intent', intent.id); + + const orderData = await (await DB.getInstance()).db + .collection('shop.orders') + .findOne({ 'paymentIntent.stripePaymentIntentData.id': intent.id }); + + if (!orderData) { + console.error('[STRIPE] Hooks - ERROR', 'no order found') + return; + } + + switch (event['type']) { + case 'payment_intent.succeeded': + // Get the order + try { + // Update the payment Intent and it's status + const dataToSave: Partial = { + paymentStatus: PaymentStatusEnum.paid, + orderStatus: OrderStatusEnum.processing, + }; + + // check if organisations ids on product + // const providersData: ProviderSchema[] = []; + // await setProviderData(orderData, await createApolloContext(), providersData); + + // if (providersData.length > 0) dataToSave.providerOrderItems = providersData; + + await (await DB.getInstance()).db.collection('shop.orders').updateOne({ _id: orderData._id }, { $set: dataToSave }); + + await afterPaymentSuccess(orderData); + } catch (error) { + console.error(error); + } + break; + case 'payment_intent.payment_failed': + const message = intent?.last_payment_error && intent?.last_payment_error.message; + console.log('Failed:', intent.id, message); + + // // Get the order + try { + // Update the payment Intent and it's status + const dataToSave: Partial = { + paymentStatus: undefined, + orderStatus: OrderStatusEnum.draft, + }; + await (await DB.getInstance()).db.collection('shop.orders').updateOne({ _id: orderData._id }, { $set: dataToSave }); + + await afterPaymentFailed(orderData); + } catch (error) { + console.error(error); + } + break; + } + + response.sendStatus(200); +}); + +module.exports.handler = serverless(app); + + diff --git a/services/module-payments/__tests/billingInfo.playground.graphql b/services/module-payments/__tests/billingInfo.playground.graphql new file mode 100644 index 0000000..428487a --- /dev/null +++ b/services/module-payments/__tests/billingInfo.playground.graphql @@ -0,0 +1,30 @@ +query me {me{paymentInfo{stripeInfo{customerId},paymentMethods{sPayMethodId,default,nameOnCard},billingInfos{firstName,lastName,vatNumber}}}} + + +mutation addOneBillingInfo { + accountsBillingInfosAddOne( + input:{ + firstName:"Sanawar",lastName:"Syed",email:"sanawar@makeit-studio.com",default:true, + address: {number:"11", zip:"1000", street: "Rue du college",city:"Bruxelles",country:"Belgique"} + }) + + {firstName,lastName,email} +} + +mutation editOneBillingInfo { + accountsBillingInfosEditOne( + id:"50f8e025-8645-4af5-b262-8d961ad3dca5", + input:{ + firstName:"Sanawar2",lastName:"Syed2",email:"sanawar@makeit-studio.com",default:true, + address: {number:"11", zip:"1000", street: "Rue du college",city:"Bruxelles",country:"Belgique"} + }) + + {firstName,lastName,email} +} + + +mutation deleteOneBillingInfo { + accountsBillingInfosDeleteOne( + id:"ca3b9ad2-4c78-419d-9bb5-079d6aeeccb5" + ) +} diff --git a/services/module-payments/__tests/pm.playground.graphql b/services/module-payments/__tests/pm.playground.graphql new file mode 100644 index 0000000..46c110e --- /dev/null +++ b/services/module-payments/__tests/pm.playground.graphql @@ -0,0 +1,18 @@ +query me {me{paymentInfo{stripeInfo{customerId},paymentMethods{sPayMethodId,default,nameOnCard},billingInfos{firstName,lastName,vatNumber}}}} + +mutation addOnePaymentMethod { + accountsPaymentMethodsAddOne( + input:{ + sPayMethodId:"pm_card_visa", + nameOnCard:"SANAWAR SYED AZOR ALI", + }) + + {nameOnCard, cardInfo{brand,country,exp_year,exp_month,last4}} +} + + +mutation deleteOnePaymentMethod { + accountsPaymentMethodsDeleteOne( + id:"4bf3e82a-af7e-4425-86d9-a84ab79a44fe" + ) +} \ No newline at end of file diff --git a/services/module-payments/components/components.errors.ts b/services/module-payments/components/components.errors.ts new file mode 100644 index 0000000..956b4c1 --- /dev/null +++ b/services/module-payments/components/components.errors.ts @@ -0,0 +1,8 @@ +export const enginePaymentErrorConfig = { + /* Payments error */ + 6000: 'There was an error with the payment provider', + 6001: 'You have no account registered with the payment provider', + + 6003: 'There are no lines in your cart', + 6004: 'There was an error with the paiement intent', +}; diff --git a/services/module-payments/components/components.orders.ts b/services/module-payments/components/components.orders.ts new file mode 100644 index 0000000..89a59d0 --- /dev/null +++ b/services/module-payments/components/components.orders.ts @@ -0,0 +1,85 @@ +import { registerEnumType, ObjectType, Field, InputType, Int } from 'type-graphql'; +import { PaymentStatusEnum } from './components.payments'; +import { AvailableTranslation } from '@src/__components/components'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ +export enum OrderTypeEnum { + online = 'online', + withShipping = 'withShipping', +} +registerEnumType(OrderTypeEnum, { + name: 'OrderType', +}); + +export enum OrderStatusEnum { + draft = 'draft', + processing = 'processing', + shipped = 'shipped', + completed = 'completed', + canceled = 'canceled', + refund = 'refund', +} +registerEnumType(OrderStatusEnum, { + name: 'OrderStatusEnum', +}); + +export enum OrderBusinessStatusEnum { + processing = 'processing', + shipped = 'shipped', + completed = 'completed', + canceled = 'canceled', + refund = 'refund', +} +registerEnumType(OrderBusinessStatusEnum, { + name: 'OrderBusinessStatusEnum', +}); + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@ObjectType() +@InputType('LocaleInfoInput') +export class LocaleInfo { + @Field({ nullable: true }) + countryCode?: string; + + @Field(() => AvailableTranslation, { nullable: true }) + locale?: AvailableTranslation; +} + +@ObjectType() +export class OrderInfoComponent { + @Field() + _id: string; + + @Field(() => OrderStatusEnum) + orderStatus: OrderStatusEnum; + + @Field(() => PaymentStatusEnum) + paymentStatus: PaymentStatusEnum; +} + +@ObjectType() +@InputType('AddOnInput') +export class AddOnComponent { + @Field() + addOnId: string; + + @Field() + quantity: number; +} diff --git a/services/module-payments/components/components.payments.ts b/services/module-payments/components/components.payments.ts new file mode 100644 index 0000000..18841d0 --- /dev/null +++ b/services/module-payments/components/components.payments.ts @@ -0,0 +1,205 @@ +import { registerEnumType, ObjectType, Field, InputType, Int } from 'type-graphql'; +import { StripePaymentIntentData } from './stripe.components'; +import { BillingAddressComponent } from './components'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +export enum cardTypeEnum { + 'alipay' = 'alipay', + 'au_becs_debit' = 'au_becs_debit', + 'bacs_debit' = 'bacs_debit', + 'bancontact' = 'bancontact', + 'card' = 'card', + 'eps' = 'eps', + 'fpx' = 'fpx', + 'giropay' = 'giropay', + 'ideal' = 'ideal', + 'p24' = 'p24', + 'sepa_debit' = 'sepa_debit', + 'sofort' = 'sofort', +} +registerEnumType(cardTypeEnum, { + name: 'cardTypeEnum', +}); + +export enum cardBrandEnum { + amex = 'amex', + diners = 'diners', + discover = 'discover', + jcb = 'jcb', + mastercard = 'mastercard', + unionpay = 'unionpay', + visa = 'visa', + unknown = 'unknown', +} +registerEnumType(cardBrandEnum, { + name: 'cardBrandEnum', +}); + +export enum PaymentStatusEnum { + pending = 'pending', + actionNeeded = 'actionNeeded', + paid = 'paid', + free = 'free', +} +registerEnumType(PaymentStatusEnum, { + name: 'PaymentStatusEnum', +}); + +export enum PaymentProvider { + free = 'free', + stripe = 'stripe', + bancontact = 'bancontact', + bankWire = 'bankWire', +} +registerEnumType(PaymentProvider, { + name: 'PaymentProvider', +}); + +export enum CurrencyEnum { + eur = 'eur', + usd = 'usd', +} +registerEnumType(CurrencyEnum, { + name: 'CurrencyEnum', +}); + +export enum InvoicingProvider { + directInvoice = 'directInvoice', +} +registerEnumType(InvoicingProvider, { + name: 'InvoicingProvider', +}); + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@ObjectType() +export class StripeInfo { + @Field() + customerId: string; +} + +@ObjectType() +export class StripePayoutInfo { + @Field() + accountId: string; + + @Field() + chargesEnabled: boolean; + @Field() + payoutsEnabled: boolean; + @Field() + detailsSubmitted: boolean; +} + +@ObjectType() +export class CardInfo { + @Field(() => cardBrandEnum) + brand: cardBrandEnum; + + @Field() + country: string; + @Field(() => Int) + exp_month: number; + @Field(() => Int) + exp_year: number; + @Field() + last4: string; +} + +@ObjectType() +export class PaymentMethodDetail { + @Field() + _id: string; + + @Field() + sPayMethodId: string; + + @Field() + default: boolean; + + @Field() + nameOnCard: string; + + @Field(() => cardTypeEnum) + type: cardTypeEnum; + + @Field(() => CardInfo, { nullable: true }) + cardInfo?: CardInfo; +} + +@InputType() +export class PaymentMethodInput { + @Field() + sPayMethodId: string; + + @Field() + nameOnCard: string; + + @Field({ nullable: true }) + default: boolean; +} + +@ObjectType() +export class AccountPaymentInfo { + @Field(() => StripeInfo, { nullable: true }) + stripeInfo?: StripeInfo; + + @Field(() => [PaymentMethodDetail], { nullable: true }) + paymentMethods?: PaymentMethodDetail[]; + + @Field(() => [BillingAddressComponent], { nullable: true }) + billingInfos?: BillingAddressComponent[]; +} + +@ObjectType() +export class AccountPayoutInfo { + @Field(() => StripePayoutInfo, { nullable: true }) + stripeInfo?: StripePayoutInfo; +} + +@ObjectType() +export class PaymentInfo { + @Field(() => PaymentProvider) + provider: PaymentProvider; + + @Field() + transactionId: string; +} + +@ObjectType() +export class PaymentIntentInfo { + @Field(() => PaymentProvider) + provider: PaymentProvider; + + @Field(() => StripePaymentIntentData) + stripePaymentIntentData?: StripePaymentIntentData; +} + +@ObjectType() +export class InvoiceInfo { + @Field(() => InvoicingProvider) + provider: InvoicingProvider; + + @Field() + invoiceId: string; + + @Field(() => Number) + invoiceNumber: number; +} diff --git a/services/module-payments/components/components.ts b/services/module-payments/components/components.ts new file mode 100644 index 0000000..a09a4b5 --- /dev/null +++ b/services/module-payments/components/components.ts @@ -0,0 +1,62 @@ +import { registerEnumType, ObjectType, Field, InputType, Int } from 'type-graphql'; +import { GraphQLJSONObject } from 'graphql-type-json'; +import { StripePaymentIntentData } from './stripe.components'; +import { AddressStrictComponent } from '@seed/interfaces/components.geo'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +/* + + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ +██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ +██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ +██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ +╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + +*/ + +@ObjectType() +@InputType('BillingAddressInput') +export class BillingAddressInput { + @Field() + firstName: string; + + @Field() + lastName: string; + + @Field(() => AddressStrictComponent) + address: AddressStrictComponent; + + @Field({ nullable: true }) + email?: string; + + @Field({ nullable: true }) + default?: boolean; + + @Field(() => GraphQLJSONObject, { nullable: true }) + customTags?: any; + + @Field({ nullable: true }) + company?: string; + + @Field({ nullable: true }) + vatNumber?: string; + + @Field({ nullable: true }) + fiscalForm?: string; +} + +@ObjectType() +@InputType('BillingAddress') +export class BillingAddressComponent extends BillingAddressInput { + @Field() + _id: string; +} diff --git a/services/module-payments/components/stripe.components.ts b/services/module-payments/components/stripe.components.ts new file mode 100644 index 0000000..299d908 --- /dev/null +++ b/services/module-payments/components/stripe.components.ts @@ -0,0 +1,48 @@ +import { Field, ObjectType } from 'type-graphql'; + +@ObjectType() +export class StripePaymentIntentData { + @Field() + id: string; + @Field({ nullable: true }) + client_secret?: string; + @Field() + currency: string; + @Field() + customer: string; + @Field() + status: string; + + // object: string; + // amount: number; + // amount_capturable: number; + // amount_received: number; + // application: any; + // application_fee_amount: any; + // canceled_at: any; + // cancellation_reason: any; + // capture_method: string; + // charges: Charges; + // confirmation_method: string; + // created: number; + + // description: any; + // invoice: any; + // last_payment_error: any; + // livemode: boolean; + // metadata: Metadata; + // next_action: any; + // on_behalf_of: any; + // payment_method: any; + // payment_method_options: PaymentMethodOptions; + // payment_method_types: string[]; + // receipt_email: any; + // review: any; + // setup_future_usage: any; + // shipping: any; + // source: any; + // statement_descriptor: any; + // statement_descriptor_suffix: any; + // transfer_data: any; + // transfer_group: any; +} diff --git a/services/module-payments/functions/billingInfos/billingInfo.resolver.admin.ts b/services/module-payments/functions/billingInfos/billingInfo.resolver.admin.ts new file mode 100644 index 0000000..34c1f93 --- /dev/null +++ b/services/module-payments/functions/billingInfos/billingInfo.resolver.admin.ts @@ -0,0 +1,14 @@ +import AccountModel from '@src/accounts/account.model'; +import { Resolver } from 'type-graphql'; + +@Resolver(AccountModel) +export default class AccountBillingInfoAdminResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-payments/functions/billingInfos/billingInfo.resolver.fields.ts b/services/module-payments/functions/billingInfos/billingInfo.resolver.fields.ts new file mode 100644 index 0000000..c1ad3c3 --- /dev/null +++ b/services/module-payments/functions/billingInfos/billingInfo.resolver.fields.ts @@ -0,0 +1,14 @@ +import AccountModel from '@src/accounts/account.model'; +import { Resolver } from 'type-graphql'; + +@Resolver(AccountModel) +export default class AccountBillingInfoFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-payments/functions/billingInfos/billingInfo.resolver.ts b/services/module-payments/functions/billingInfos/billingInfo.resolver.ts new file mode 100644 index 0000000..e8a97d9 --- /dev/null +++ b/services/module-payments/functions/billingInfos/billingInfo.resolver.ts @@ -0,0 +1,75 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; +import AccountModel, { NewAccountInput } from '@src/accounts/account.model'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { signinGeneric } from '@services/module-accounts/services/AccountService'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { BillingAddressComponent, BillingAddressInput } from '@services/module-payments/components/components'; + +@Resolver(AccountModel) +export default class AccountBillingInfoResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => BillingAddressComponent) + @Authorized() + async accountsBillingInfosAddOne(@Arg('input') input: BillingAddressInput, @Ctx() ctx: ApolloContext): Promise { + const model = ctx.ctx.user; + + if (input.default && model.paymentInfo && model.paymentInfo.billingInfos) { + // Remove all default from others + await model.updateOneCustom( + { _id: model._id }, + { + $set: { 'paymentInfo.billingInfos.$[].default': false }, + }, + ctx, + ); + } else input.default = false; + + return await model.addOneSubRessource({ _id: model._id }, 'paymentInfo.billingInfos', input, ctx); + } + + @Mutation(() => BillingAddressComponent) + @Authorized() + async accountsBillingInfosEditOne( + @Arg('id') id: string, + @Arg('input') input: BillingAddressInput, + @Ctx() ctx: ApolloContext, + ): Promise { + const model = ctx.ctx.user; + + if (input.default) { + // Remove all default from others + await model.updateOneCustom( + { _id: model._id }, + { + $set: { 'paymentInfo.billingInfos.$[].default': false }, + }, + ctx, + ); + } else input.default = false; + + return await model.editOneSubRessource({ _id: model._id }, 'paymentInfo.billingInfos', id, input, ctx); + } + + @Mutation(() => String) + @Authorized() + async accountsBillingInfosDeleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = ctx.ctx.user; + return await model.deleteOneSubRessource({ _id: model._id }, 'paymentInfo.billingInfos', id, ctx); + } +} diff --git a/services/module-payments/functions/orders/__index.ts b/services/module-payments/functions/orders/__index.ts new file mode 100644 index 0000000..ac3046e --- /dev/null +++ b/services/module-payments/functions/orders/__index.ts @@ -0,0 +1,10 @@ +import 'reflect-metadata'; +import OrderEngineResolver from './resolvers/app/app.resolver'; +import OrderAdminEngineQueryResolver from './resolvers/admin/admin.queries'; +import OrderAdminEngineMutationResolver from './resolvers/admin/admin.mutations'; +import OrderBusinessEngineQueryResolver from './resolvers/business/business.queries'; +import OrderBusinessEngineMutationResolver from './resolvers/business/business.mutations'; + +export const OrderEngineResolvers = [OrderEngineResolver]; +export const OrderAdminEngineResolvers = [OrderAdminEngineQueryResolver, OrderAdminEngineMutationResolver]; +export const OrderBusinessEngineResolvers = [OrderBusinessEngineQueryResolver, OrderBusinessEngineMutationResolver]; diff --git a/services/module-payments/functions/orders/carts/__index.ts b/services/module-payments/functions/orders/carts/__index.ts new file mode 100644 index 0000000..f103a45 --- /dev/null +++ b/services/module-payments/functions/orders/carts/__index.ts @@ -0,0 +1,6 @@ +import 'reflect-metadata'; + +import CartEngineResolver from '@services/module-payments/functions/orders/carts/resolvers/cart.resolver'; + +export const CartEngineResolvers = [CartEngineResolver]; +export const CartEngineAdminResolvers = []; diff --git a/services/module-payments/functions/orders/carts/resolvers/cart.resolver.ts b/services/module-payments/functions/orders/carts/resolvers/cart.resolver.ts new file mode 100644 index 0000000..78530f5 --- /dev/null +++ b/services/module-payments/functions/orders/carts/resolvers/cart.resolver.ts @@ -0,0 +1,274 @@ +import _ from 'lodash'; +import { ApolloError } from 'apollo-server-lambda'; + +import { Resolver, Authorized, Ctx, Query } from 'type-graphql'; +import { Mutation, Arg } from 'type-graphql'; + +import { ApolloContext } from '@seed/interfaces/context'; +import { updatePaymentIntent } from '@services/module-payments/helpers/payments.helpers'; +import OrderEngineModel from '../../order.model'; +import PromoModel, { validatePromo } from '@services/module-payments/functions/promos/promo.model'; +import { PaymentIntentInfo } from '@services/module-payments/components/components.payments'; +import { addToCart, getCurrentCartOrder } from '@services/module-payments/functions/orders/services/CartService'; +import { LineItemBaseSchema } from '@src/orders/components/line.schema'; +import { OrderEngineSchema } from '../../schemas/order.schema'; +import { + CartEngineAddCountryCodeSchema, + CartEngineAddPromoSchema, + CartEngineUpdateOrDeleteSchema, + CartEngineUpdateSchema, +} from '../schemas/cart.schema.inputs'; +import { OrderNewInputSchema } from '@src/orders/components/components.cart'; + +@Resolver(OrderEngineSchema) +export default class CartEngineResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => OrderEngineSchema) + @Authorized() + async cartsGetMe(@Ctx() ctx: ApolloContext, @Arg('input', { nullable: true }) input?: OrderNewInputSchema): Promise { + try { + return await getCurrentCartOrder(ctx, input); + } catch (error) { + throw error; + } + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => OrderEngineSchema) + @Authorized() + async cartsAddOneItem(@Arg('input') input: LineItemBaseSchema, @Ctx() ctx: ApolloContext): Promise { + return await addToCart(input, ctx); + } + + @Mutation(() => OrderEngineSchema) + @Authorized() + async cartsUpdateOneItem(@Arg('input') input: CartEngineUpdateSchema, @Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + const orderModel = new OrderEngineModel(); + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + // Check the lines + const lineIndex = _.findIndex(currentCartOrder.lines, function(o) { + if (o.productId == input.productId && _.isEqual(JSON.parse(JSON.stringify(o.options)), JSON.parse(JSON.stringify(o.options)))) + return true; + else return false; + }); + + if (lineIndex === -1) throw new ApolloError('cart.item.notFound', '404', { productId: input.productId }); + + if (input.newQuantity == 0) currentCartOrder.lines.splice(lineIndex, 1); + else { + // Update the quantity + currentCartOrder.lines[lineIndex].quantity = input.newQuantity; + } + + const paymentIntentUpdated: PaymentIntentInfo = await updatePaymentIntent(currentCartOrder.paymentIntent, { + amount: await currentCartOrder.finalPrice(ctx), + currency: currentCartOrder.currency, + account, + }); + + return await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $set: { + paymentIntent: paymentIntentUpdated, + lines: currentCartOrder.lines, + }, + }, + ctx, + }); + } catch (error) { + throw error; + } + } + + @Mutation(() => OrderEngineSchema) + @Authorized() + async cartsUpdateOrDeleteOneItem(@Arg('input') input: CartEngineUpdateOrDeleteSchema, @Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + const orderModel = new OrderEngineModel(); + + const { lineId, newQuantity } = input; + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + // Check the lines + const lineIndex = _.findIndex(currentCartOrder.lines, { _id: lineId }); + + if (lineIndex === -1) throw new ApolloError('cart.line.notFound', '404', { lineId: input.lineId }); + + if (newQuantity == 0) currentCartOrder.lines.splice(lineIndex, 1); + else { + // Update the quantity + currentCartOrder.lines[lineIndex].quantity = newQuantity; + } + + const paymentIntentUpdated: PaymentIntentInfo = await updatePaymentIntent(currentCartOrder.paymentIntent, { + amount: await currentCartOrder.finalPrice(ctx), + currency: currentCartOrder.currency, + account, + }); + + return await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $set: { + paymentIntent: paymentIntentUpdated, + lines: currentCartOrder.lines, + }, + }, + ctx, + }); + } catch (error) { + throw error; + } + } + + @Mutation(() => OrderEngineSchema) + @Authorized() + async cartsEmpty(@Ctx() ctx: ApolloContext): Promise { + const orderModel = new OrderEngineModel(); + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + return await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $set: { + lines: [], + }, + $unset: { + promoId: '', + countryCode: '', + locale: '', + }, + }, + ctx, + }); + } catch (error) { + throw error; + } + } + + @Mutation(() => OrderEngineSchema) + @Authorized() + async cartAddCountryCode(@Arg('input') input: CartEngineAddCountryCodeSchema, @Ctx() ctx: ApolloContext): Promise { + const orderModel = new OrderEngineModel(); + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + if (!currentCartOrder) throw new ApolloError('cart.notFound', '404'); + + const lines = currentCartOrder.lines; + lines.map((line) => { + if (line.localeInfo) return (line.localeInfo.countryCode = input.countryCode); + return (line.localeInfo = { countryCode: input.countryCode }); + }); + + return await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $set: { countryCode: input.countryCode, lines }, + }, + ctx, + }); + } catch (error) { + throw error; + } + } + + @Mutation(() => OrderEngineSchema) + @Authorized() + async cartAddPromo(@Arg('input') input: CartEngineAddPromoSchema, @Ctx() ctx: ApolloContext): Promise { + const orderModel = new OrderEngineModel(); + const promoModel = new PromoModel(); + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + + if (!currentCartOrder) throw new ApolloError('cart.notFound', '404'); + + await promoModel.getOne({ code: input.promoCode }, ctx); + await validatePromo(input.promoCode, ctx); + + const lines = currentCartOrder.lines; + lines.map((line) => { + return (line.promoId = promoModel._id); + }); + + const paymentIntentUpdated: PaymentIntentInfo = await updatePaymentIntent(currentCartOrder.paymentIntent, { + amount: await currentCartOrder.finalPrice(ctx), + currency: currentCartOrder.currency, + account: ctx.ctx.user, + }); + + return await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + // finalPrice: currentCart.finalPrice, + $set: { promoId: promoModel._id, lines, paymentIntent: paymentIntentUpdated }, + }, + ctx, + }); + } catch (error) { + throw error; + } + } + + @Mutation(() => OrderEngineSchema) + @Authorized() + async cartRemovePromo(@Ctx() ctx: ApolloContext): Promise { + const orderModel = new OrderEngineModel(); + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + + if (!currentCartOrder) throw new ApolloError('cart.notFound', '404'); + + const lines = currentCartOrder.lines; + lines.forEach((line) => { + delete line.promoId; + }); + + const paymentIntentUpdated: PaymentIntentInfo = await updatePaymentIntent(currentCartOrder.paymentIntent, { + amount: await currentCartOrder.finalPrice(ctx), + currency: currentCartOrder.currency, + account: ctx.ctx.user, + }); + + return await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $unset: { + promoId: '', + }, + $set: { + lines, + paymentIntent: paymentIntentUpdated, + }, + }, + ctx, + }); + } catch (error) { + throw error; + } + } +} diff --git a/services/module-payments/functions/orders/carts/schemas/cart.schema.inputs.ts b/services/module-payments/functions/orders/carts/schemas/cart.schema.inputs.ts new file mode 100644 index 0000000..42e08dc --- /dev/null +++ b/services/module-payments/functions/orders/carts/schemas/cart.schema.inputs.ts @@ -0,0 +1,30 @@ +import { AvailableTranslation } from '@src/__components/components'; +import { InputType, Field } from 'type-graphql'; + +@InputType() +export class CartEngineUpdateSchema { + @Field() + productId: string; + @Field() + newQuantity: number; +} + +@InputType() +export class CartEngineUpdateOrDeleteSchema { + @Field() + lineId: string; + @Field() + newQuantity: number; +} + +@InputType() +export class CartEngineAddPromoSchema { + @Field() + promoCode: string; +} + +@InputType() +export class CartEngineAddCountryCodeSchema { + @Field() + countryCode: string; +} diff --git a/services/module-payments/functions/orders/carts/schemas/line.schema.ts b/services/module-payments/functions/orders/carts/schemas/line.schema.ts new file mode 100644 index 0000000..f909952 --- /dev/null +++ b/services/module-payments/functions/orders/carts/schemas/line.schema.ts @@ -0,0 +1,134 @@ +import { ObjectType, Field, InputType } from 'type-graphql'; +import { ProductRessourceEnum } from '@src/__components/components'; +import { GraphQLJSONObject } from 'graphql-type-json'; +import { AddOnComponent, LocaleInfo } from '@services/module-payments/components/components.orders'; + +export interface LineProductInterface { + _id: string; + + productRessource: ProductRessourceEnum; + productId: string; + + title: string; + + price: number; + salesPrice?: number; + + quantity: number; + + vatClassId?: string; + + // vatPrice(): number; + + finalPrice(): number; + + // linePrice(): number; + + organisationId?: string; + parentId?: string; + + addOns?: AddOnComponent[]; + + localeInfo?: LocaleInfo; +} + +export interface LineProductFunctionInterface { + validateCartOption?(data: any): any; + getLinePrice?(data: any): any; +} + +@ObjectType() +export class LineProductClass implements LineProductInterface { + // linePrice(): number { + // throw new Error('Method not implemented.'); + // } + @Field() + _id: string; + + @Field(() => ProductRessourceEnum) + productRessource: ProductRessourceEnum; + + @Field() + productId: string; + + @Field() + title: string; + + @Field() + price: number; + @Field() + salesPrice?: number; + + @Field() + quantity: number; + + @Field() + vatClassId?: string; + + @Field({ nullable: true }) + parentId?: string; + + @Field(() => [AddOnComponent], { nullable: true }) + addOns?: AddOnComponent[]; + + @Field(() => LocaleInfo, { nullable: true }) + localeInfo?: LocaleInfo; + + @Field({ nullable: true }) + promoId?: string; + + + // @Field() + // vatPrice(): number { + // throw 'Implements on parent'; + // } + + validateCartOption?(): any; + organisationId?: string; + + @Field({ nullable: true }) + finalPrice(): number { + if (this.salesPrice) return this.salesPrice; + else return this.price; + } +} + +@ObjectType() +@InputType('LineProductBaseInput') +export class LineProductBase { + @Field(() => ProductRessourceEnum) + productRessource: ProductRessourceEnum; + + @Field() + productId: string; + + @Field() + quantity: number; + + @Field({ nullable: true }) + parentId?: string; + + @Field(() => GraphQLJSONObject, { nullable: true }) + options?: any; + + @Field(() => [AddOnComponent], { nullable: true }) + addOns?: AddOnComponent[]; +} + +@ObjectType() +@InputType('LineProductBaseSuppInput') +export class LineProductBaseSupp extends LineProductBase { + @Field() + parentId: string; +} + +export interface LineProductBaseInterface { + getLineProduct(input: { quantity: number }): LineProductInterface; + getLineProductFunctions(input: any): LineProductFunctionInterface; +} + +@InputType('LineItemGenericSchemaInput') +@ObjectType() +export abstract class LineItemGenericSchema extends LineProductClass { + options?: any; +} diff --git a/services/module-payments/functions/orders/helpers/OrderHelper.ts b/services/module-payments/functions/orders/helpers/OrderHelper.ts new file mode 100644 index 0000000..142acdb --- /dev/null +++ b/services/module-payments/functions/orders/helpers/OrderHelper.ts @@ -0,0 +1,35 @@ +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { OrderEngineDBInterfaceSchema } from '../schemas/order.schema'; + +export async function getTotalPrice(order: OrderEngineDBInterfaceSchema, ctx: ApolloContext): Promise { + let price = 0; + if (order.lines && order.lines.length > 0) { + for (let index = 0; index < order.lines.length; index++) { + const line = order.lines[index]; + price += (await line.getLinePrice(ctx)) + (await line.getLineVatPrice(ctx)); + } + } + return price; +} + +export async function getVatPrice(order: OrderEngineDBInterfaceSchema, ctx: ApolloContext): Promise { + let price = 0; + if (order.lines && order.lines.length > 0) { + for (let index = 0; index < order.lines.length; index++) { + const line = order.lines[index]; + price += await line.getLineVatPrice(ctx); + } + } + return price; +} + +export async function getSubTotalPrice(order: OrderEngineDBInterfaceSchema, ctx: ApolloContext): Promise { + let price = 0; + if (order.lines && order.lines.length > 0) { + for (let index = 0; index < order.lines.length; index++) { + const line = order.lines[index]; + price += await line.getLinePrice(ctx); + } + } + return price; +} diff --git a/services/module-payments/functions/orders/order.model.ts b/services/module-payments/functions/orders/order.model.ts new file mode 100644 index 0000000..e18e4f8 --- /dev/null +++ b/services/module-payments/functions/orders/order.model.ts @@ -0,0 +1,60 @@ +import { ObjectType, Field, ID, ArgsType, InputType } from 'type-graphql'; + +import { Permission } from '@seed/interfaces/permission'; + +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { OrderEngineDBInterfaceSchema, OrderEngineDBSchema, OrderEngineSchema } from './schemas/order.schema'; +import { EngineModel } from '@lib/seed/engine/EngineModel'; +import { plainToClass } from 'class-transformer'; +import { IEngineSchema } from '@lib/seed/engine/EngineSchema'; +import { OrderArgs } from './schemas/order.schema.input'; +import { CustomSearchEngine } from '@lib/seed/services/database/DBRequestService'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; +@ObjectType() +export default class OrderEngineModel extends EngineModel { + public constructor(input?: OrderEngineDBInterfaceSchema & Partial) { + const dataInit = plainToClass(OrderEngineDBSchema, input || {}); + super({ + // ...init, + collectionName: 'shop.orders', + permissions: permissions, + dataInit, + }); + } + + plainToClass(plain: any): OrderEngineSchema | OrderEngineSchema[] { + return plainToClass(OrderEngineSchema, plain); + } + + searchEngine(): CustomSearchEngine { + const sEngine: CustomSearchEngine = { + orderStatus: { + operation: '$eq', + }, + paymentStatus: { + operation: '$eq', + }, + search: { + operation: 'freeInput', + freeInputConfig: { + searchOptions: ['billingInfo.company', 'billingInfo.email'], + }, + }, + }; + return sEngine; + } + + async afterPaymentSuccess(orderData: OrderEngineSchema): Promise { + throw 'To Implement on parent'; + } + + async afterPaymentFailed(orderData: OrderEngineSchema): Promise { + throw 'To Implement on parent'; + } +} diff --git a/services/module-payments/functions/orders/resolvers/admin/admin.mutations.ts b/services/module-payments/functions/orders/resolvers/admin/admin.mutations.ts new file mode 100644 index 0000000..c9bab71 --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/admin/admin.mutations.ts @@ -0,0 +1,116 @@ +import { Arg, Args, Ctx, Mutation, Resolver, Authorized } from 'type-graphql'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import OrderEngineModel from '@services/module-payments/functions/orders/order.model'; +import { OrderEngineSchema } from '../../schemas/order.schema'; +import { createBasePublicResolver } from '@seed/graphql/baseResolvers/BasePublicResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ApolloError } from 'apollo-server-lambda'; + +import StripeService from '@services/module-payments/services/stripe/StripeService'; +import { OrderStatusEnum } from '@services/module-payments/components/components.orders'; +import { PaymentStatusEnum } from '@services/module-payments/components/components.payments'; +import { AsyncHooksService } from '@lib/__hooks'; + +@Resolver(OrderEngineModel) +export default class OrderAdminEngineMutationResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => OrderEngineSchema) + @Authorized(AccountTypeEnum.admin) + async ordersMarkOneAsPaid(@Arg('id') orderId: string, @Ctx() ctx: ApolloContext): Promise { + const model = new OrderEngineModel(); + const currentOrder = await model.getOne({ query: { _id: orderId }, ctx }); + + if (currentOrder.paymentStatus == PaymentStatusEnum.paid) throw new ApolloError('orders.payment.alreadypaid', '400'); + if (currentOrder.paymentStatus == PaymentStatusEnum.free) throw new ApolloError('orders.payment.freeItem', '400'); + + const result = await model.updateOne({ + query: { _id: currentOrder._id }, + newData: { + paymentStatus: PaymentStatusEnum.paid, + }, + ctx, + }); + await new AsyncHooksService().afterOrdersMarkOneAsPaid(result, ctx); + return result; + } + + @Mutation(() => OrderEngineSchema) + @Authorized(AccountTypeEnum.admin) + async ordersMarkOneAsUnPaid(@Arg('id') orderId: string, @Ctx() ctx: ApolloContext): Promise { + const model = new OrderEngineModel(); + const currentOrder = await model.getOne({ query: { _id: orderId }, ctx }); + + if (currentOrder.paymentStatus == PaymentStatusEnum.pending) throw new ApolloError('orders.payment.alreadyUnPaid', '400'); + if (currentOrder.paymentStatus == PaymentStatusEnum.free) throw new ApolloError('orders.payment.freeItem', '400'); + + const result = await model.updateOne({ + query: { _id: currentOrder._id }, + newData: { + paymentStatus: PaymentStatusEnum.pending, + }, + ctx, + }); + await new AsyncHooksService().afterOrdersMarkOneAsUnPaid(result, ctx); + return result; + } + + @Mutation(() => OrderEngineSchema) + @Authorized(AccountTypeEnum.admin) + async ordersCancelOne(@Arg('id') orderId: string, @Ctx() ctx: ApolloContext): Promise { + const model = new OrderEngineModel(); + const currentOrder = await model.getOne({ query: { _id: orderId }, ctx }); + + if (currentOrder.orderStatus == OrderStatusEnum.canceled) throw new ApolloError('orders.alreadyCancel', '400'); + if (currentOrder.orderStatus == OrderStatusEnum.refund) throw new ApolloError('orders.alreadyRefund', '400'); + + const result = await model.updateOne({ + query: { _id: currentOrder._id }, + newData: { + orderStatus: OrderStatusEnum.canceled, + }, + ctx, + }); + await new AsyncHooksService().afterOrderCancel(result, ctx); + return result; + } + + @Mutation(() => OrderEngineSchema) + @Authorized(AccountTypeEnum.admin) + async ordersReimburseOne(@Arg('orderId') orderId: string, @Ctx() ctx: ApolloContext): Promise { + const model = new OrderEngineModel(); + const currentOrder = await model.getOne({ query: { _id: orderId }, ctx }); + if (currentOrder.orderStatus != OrderStatusEnum.canceled) throw new ApolloError('orders.reimburse.notCanceled', '400'); + try { + if (currentOrder.paymentInfo?.transactionId) { + await StripeService.getInstance().reimburse(currentOrder.paymentInfo.transactionId); + const result = await model.updateOne({ + query: { _id: currentOrder._id }, + newData: { + orderStatus: OrderStatusEnum.refund, + }, + ctx, + }); + await new AsyncHooksService().afterOrdersReimbursed(result, ctx); + return result; + } + throw new ApolloError('orders.reimburse.noTransactionFound', '400'); + } catch (error) { + throw error; + } + } +} diff --git a/services/module-payments/functions/orders/resolvers/admin/admin.queries.ts b/services/module-payments/functions/orders/resolvers/admin/admin.queries.ts new file mode 100644 index 0000000..4ae1526 --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/admin/admin.queries.ts @@ -0,0 +1,46 @@ +import { Arg, Args, Ctx, Query, Resolver } from 'type-graphql'; + +import OrderModel from '@services/module-payments/functions/orders/order.model'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { OrderEngineSchema } from '../../schemas/order.schema'; +import { OrderArgs } from '../../schemas/order.schema.input'; +import { EngineMiddleware } from '@lib/seed/graphql/MiddlewareV2'; + +@Resolver(OrderModel) +export default class OrderAdminEngineQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => OrderEngineSchema) + @EngineMiddleware({ + authorization: [AccountTypeEnum.admin], + }) + async adminsOrdersGetOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + return new OrderModel().getOneGeneric(id, ctx); + } + + @Query(() => [OrderEngineSchema]) + @EngineMiddleware({ + authorization: [AccountTypeEnum.admin], + }) + async adminsOrdersGetMany(@Args() args: OrderArgs, @Ctx() ctx: ApolloContext): Promise { + const results = await new OrderModel().getManyGenericWithArgs(args, ctx); + return results; + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-payments/functions/orders/resolvers/app/app.resolver.ts b/services/module-payments/functions/orders/resolvers/app/app.resolver.ts new file mode 100644 index 0000000..652a16e --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/app/app.resolver.ts @@ -0,0 +1,86 @@ +import { Resolver, Authorized, Query, Ctx, Arg, Args, Mutation } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; + +import OrderModel from '@services/module-payments/functions/orders/order.model'; +import { OrderEngineSchema } from '../../schemas/order.schema'; +import { OrderArgs } from '../../schemas/order.schema.input'; +import { getCurrentCartOrder } from '../../services/CartService'; +import { newError } from '@seed/helpers/Error'; +import { localCheckout } from '../../services/OrderService'; + +@Resolver(OrderModel) +export default class OrderEngineResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => OrderEngineSchema) + @Authorized() + async myCurrentOrder(@Ctx() ctx: ApolloContext): Promise { + const cartOrderData = await getCurrentCartOrder(ctx); + if (!cartOrderData) throw newError(6003); + + return cartOrderData; + } + + @Query(() => OrderEngineSchema) + @Authorized() + async myOrdersGetOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + + return new OrderModel().getOne({ query: { _id: id, accountId: account._id } }); + } + + @Query(() => [OrderEngineSchema]) + @Authorized() + async myOrdersGetMany(@Args() arg: OrderArgs, @Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + const args = { ...arg, accountId: account._id }; + + return new OrderModel().getManyGenericWithArgs(args, ctx); + } + + @Query(() => Number) + @Authorized() + async myOrdersGetManyCount(@Args() arg: OrderArgs, @Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + const args = { ...arg, accountId: account._id }; + + return new OrderModel().getCountGeneric(args, ctx); + } + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => OrderEngineSchema) + @Authorized() + async removeCurrentOrder(@Ctx() ctx: ApolloContext): Promise { + try { + const account = ctx.ctx.user; + return new OrderModel().deleteOne({ query: { accountId: account._id }, ctx }); + } catch (error) { + throw error; + } + } + + @Mutation(() => OrderEngineSchema) + async validateOrder(@Arg('id') id: string): Promise { + try { + const order = await new OrderModel().getOne({ query: { _id: id } }); + await localCheckout(order); + return order; + } catch (error) { + throw error; + } + } +} diff --git a/services/module-payments/functions/orders/resolvers/app/checkout.resolver.ts b/services/module-payments/functions/orders/resolvers/app/checkout.resolver.ts new file mode 100644 index 0000000..447197e --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/app/checkout.resolver.ts @@ -0,0 +1,54 @@ +import { Resolver, Authorized, Ctx } from 'type-graphql'; +import { Mutation, Arg } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; + +// From Module +import { OrderEngineSchema } from '../../schemas/order.schema'; +import { CheckoutEngineInput } from '../../schemas/order.schema.input'; + +// From SRC +import OrderModel from '@src/orders/order.model'; +import { createCheckout, finaliseCheckoutWithOrder } from '../../services/OrderService'; +import { PaymentProvider } from '@services/module-payments/components/components.payments'; +import { CheckoutInput } from '@src/orders/components/checkout.schema'; + +@Resolver(OrderModel) +export default class CheckoutEngineResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => OrderEngineSchema) + @Authorized() + async finaliseCheckoutWithStripe(@Arg('input') input: CheckoutInput, @Ctx() ctx: ApolloContext): Promise { + try { + const { billingInfo, contactInfo, ...rest } = input; + return await finaliseCheckoutWithOrder({ + type: 'default', + payment: PaymentProvider.stripe, + contactInfo: { + billingInfo, + contactInfo, + }, + input: rest, + ctx, + }); + } catch (error) { + throw error; + } + } +} diff --git a/services/module-payments/functions/orders/resolvers/business/business.input.ts b/services/module-payments/functions/orders/resolvers/business/business.input.ts new file mode 100644 index 0000000..454d827 --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/business/business.input.ts @@ -0,0 +1,7 @@ +import { InputType, Field } from 'type-graphql'; + +@InputType() +export class ChangeStatusInput { + @Field({ nullable: true }) + customMessage?: string; +} diff --git a/services/module-payments/functions/orders/resolvers/business/business.mutations.ts b/services/module-payments/functions/orders/resolvers/business/business.mutations.ts new file mode 100644 index 0000000..b2a7b41 --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/business/business.mutations.ts @@ -0,0 +1,58 @@ +import { Arg, Args, Ctx, Mutation, Query, Resolver } from 'type-graphql'; +import _ from 'lodash'; + +import OrderModel from '@services/module-payments/functions/orders/order.model'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ProviderSchema } from '../../schemas/provider.schema'; +import { EngineMiddleware } from '@lib/seed/graphql/MiddlewareV2'; +import { updateBusinessOrderStatus } from './business.service'; +import { ChangeStatusInput } from './business.input'; +import { OrderBusinessStatusEnum } from '@services/module-payments/components/components.orders'; + +@Resolver(OrderModel) +export default class OrderBusinessEngineMutationResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Mutation(() => ProviderSchema) + @EngineMiddleware({ + authorization: [AccountTypeEnum.organisationOwner, AccountTypeEnum.storeManager], + checkOrganisation: true, + }) + async businessOrdersMarkAsShipped( + @Arg('id') id: string, + @Arg('input') input: ChangeStatusInput, + @Ctx() ctx: ApolloContext, + ): Promise { + return await updateBusinessOrderStatus(id, OrderBusinessStatusEnum.shipped, ctx); + } + + @Mutation(() => ProviderSchema) + @EngineMiddleware({ + authorization: [AccountTypeEnum.organisationOwner, AccountTypeEnum.storeManager], + checkOrganisation: true, + }) + async businessOrdersMarkAsCompleted( + @Arg('id') id: string, + @Arg('input') input: ChangeStatusInput, + @Ctx() ctx: ApolloContext, + ): Promise { + return await updateBusinessOrderStatus(id, OrderBusinessStatusEnum.completed, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-payments/functions/orders/resolvers/business/business.queries.ts b/services/module-payments/functions/orders/resolvers/business/business.queries.ts new file mode 100644 index 0000000..4dba85d --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/business/business.queries.ts @@ -0,0 +1,67 @@ +import { Arg, Args, Ctx, Query, Resolver } from 'type-graphql'; + +import OrderModel from '@services/module-payments/functions/orders/order.model'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ProviderSchema } from '../../schemas/provider.schema'; +import _ from 'lodash'; +import { OrderBusinessArgs } from '../../schemas/order.schema.input'; +import { EngineMiddleware } from '@lib/seed/graphql/MiddlewareV2'; +import { OrderStatusEnum } from '@services/module-payments/components/components.orders'; +import { PaymentStatusEnum } from '@services/module-payments/components/components.payments'; + +@Resolver(OrderModel) +export default class OrderBusinessEngineQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => ProviderSchema) + @EngineMiddleware({ + authorization: [AccountTypeEnum.organisationOwner, AccountTypeEnum.storeManager], + checkOrganisation: true, + }) + async businessOrdersGetOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const organisationId = ctx.ctx.organisationId; + const order = await new OrderModel().getOne({ query: { 'providerOrderItems.organisationId': organisationId, 'providerOrderItems._id': id } }); + + return _.find(order.providerOrderItems, { _id: id }); + } + + @Query(() => [ProviderSchema], { nullable: 'itemsAndList' }) + @EngineMiddleware({ + authorization: [AccountTypeEnum.organisationOwner, AccountTypeEnum.storeManager], + checkOrganisation: true, + }) + async businessOrdersGetMany(@Args() args: OrderBusinessArgs, @Ctx() ctx: ApolloContext): Promise { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const organisationId = ctx.ctx.organisationId!; + const { pagination, ...arg } = args; + + // Get only those where orderStatus and paymentStatus is correct + + const filters: any = { + 'providerOrderItems.organisationId': organisationId, + orderStatus: { $in: [OrderStatusEnum.processing, OrderStatusEnum.shipped, OrderStatusEnum.completed] }, + paymentStatus: { $in: [PaymentStatusEnum.paid] }, + }; + if (arg.orderStatus) filters['providerOrderItems.orderStatus'] = { $in: arg.orderStatus }; + + const orders = await new OrderModel().getMany({ query: filters, pagination }); + const results: ProviderSchema[] = []; + + orders.forEach((order) => { + if (order.providerOrderItems) { + const index = _.findIndex(order.providerOrderItems, { organisationId: organisationId }); + if (index !== -1) results.push(order.providerOrderItems[index]); + } + }); + + return results; + } +} diff --git a/services/module-payments/functions/orders/resolvers/business/business.service.ts b/services/module-payments/functions/orders/resolvers/business/business.service.ts new file mode 100644 index 0000000..17470e7 --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/business/business.service.ts @@ -0,0 +1,23 @@ +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { OrderBusinessStatusEnum } from '@services/module-payments/components/components.orders'; +import { ApolloError } from 'apollo-server-lambda'; +import _ from 'lodash'; +import OrderEngineModel from '../../order.model'; +import { ProviderSchema } from '../../schemas/provider.schema'; + +export const updateBusinessOrderStatus = async (id: string, newStatus: OrderBusinessStatusEnum, ctx: ApolloContext): Promise => { + const organisationId = ctx.ctx.organisationId; + const orderModel = new OrderEngineModel(); + const order = await orderModel.getOne({ query: { 'providerOrderItems.organisationId': organisationId, 'providerOrderItems._id': id } }); + + const providerOrderItem = _.find(order.providerOrderItems, { _id: id }); + if (!providerOrderItem) throw new ApolloError('notfound', '400'); + + providerOrderItem.orderStatus = newStatus; + await (await orderModel.db()).updateOne( + { 'providerOrderItems.organisationId': organisationId, 'providerOrderItems._id': id }, + { $set: { 'providerOrderItems.$': providerOrderItem } }, + ); + + return providerOrderItem; +}; diff --git a/services/module-payments/functions/orders/resolvers/field.resolver.ts b/services/module-payments/functions/orders/resolvers/field.resolver.ts new file mode 100644 index 0000000..43d9655 --- /dev/null +++ b/services/module-payments/functions/orders/resolvers/field.resolver.ts @@ -0,0 +1,9 @@ +import { Resolver } from 'type-graphql'; +import { OrderEngineSchema } from '../schemas/order.schema'; +import { ProviderSchema } from '../schemas/provider.schema'; + +@Resolver(() => OrderEngineSchema) +export class OrderFieldResolver {} + +@Resolver(() => ProviderSchema) +export class ProviderItemFieldResolver {} diff --git a/services/module-payments/functions/orders/schemas/order.schema.input.ts b/services/module-payments/functions/orders/schemas/order.schema.input.ts new file mode 100644 index 0000000..000190a --- /dev/null +++ b/services/module-payments/functions/orders/schemas/order.schema.input.ts @@ -0,0 +1,49 @@ +import { GetArgs } from '@lib/seed/graphql/Request'; +import { LocaleInfo, OrderStatusEnum } from '@services/module-payments/components/components.orders'; +import { PaymentStatusEnum } from '@services/module-payments/components/components.payments'; + +import { AvailableTranslation, BuyerInfoComponent } from '@src/__components/components'; +import { ArgsType, Field, InputType, ObjectType } from 'type-graphql'; + +@ArgsType() +export class OrderArgs { + @Field(() => OrderStatusEnum, { nullable: true }) + orderStatus?: OrderStatusEnum; + + @Field(() => PaymentStatusEnum, { nullable: true }) + paymentStatus?: PaymentStatusEnum; + + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; + + @Field({ nullable: true }) + search?: string; +} + +@ArgsType() +export class OrderBusinessArgs { + @Field(() => [OrderStatusEnum], { nullable: true }) + orderStatus?: OrderStatusEnum[]; + + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@ObjectType() +@InputType('CheckoutEngineInputI') +export class CheckoutEngineInput { + @Field(() => BuyerInfoComponent) + contactInfo: BuyerInfoComponent; + + @Field(() => BuyerInfoComponent) + billingInfo: BuyerInfoComponent; + + @Field({ nullable: true }) + token?: string; +} + +@InputType() +export class OrderEngineNewSchema { + @Field(() => LocaleInfo, { nullable: true }) + localeInfo?: AvailableTranslation; +} diff --git a/services/module-payments/functions/orders/schemas/order.schema.ts b/services/module-payments/functions/orders/schemas/order.schema.ts new file mode 100644 index 0000000..da9ce45 --- /dev/null +++ b/services/module-payments/functions/orders/schemas/order.schema.ts @@ -0,0 +1,140 @@ +import { ObjectType, Field, InputType, ID, Ctx } from 'type-graphql'; + +import { IEngineSchema, MetaBy, MetaPermissions } from '@lib/seed/engine/EngineSchema'; +import { AvailableTranslation, BuyerInfoComponent, TranslatableComponent } from '@src/__components/components'; +import { ProviderSchema } from './provider.schema'; +import { Type } from 'class-transformer'; +import { LocaleInfo, OrderStatusEnum } from '@services/module-payments/components/components.orders'; +import { + PaymentIntentInfo, + PaymentStatusEnum, + PaymentInfo, + InvoiceInfo, + CurrencyEnum, +} from '@services/module-payments/components/components.payments'; +import { ApolloContext } from '@seed/interfaces/context'; +import { getTotalPrice, getSubTotalPrice, getVatPrice } from '../helpers/OrderHelper'; +import { PromoType } from '@services/module-payments/functions/promos/component'; +import PromoModel from '@services/module-payments/functions/promos/promo.model'; +import { LineItemSchema } from '@src/orders/components/line.schema'; +import { EnginePathComponent } from '@seed/interfaces/components'; + +@InputType() +@ObjectType() +export class OrderEngineBaseSchema { + /* + LINES + */ + + @Field(() => [LineItemSchema]) + @Type(() => LineItemSchema) + lines: LineItemSchema[]; + + /* + CURRENCY & LOCALE + */ + + @Field(() => LocaleInfo) + localeInfo: LocaleInfo; + + @Field(() => CurrencyEnum) + currency: CurrencyEnum; + + @Field(() => Boolean) + vatExempt = false; + + /* + CONTACTS + */ + + @Field() + accountId: string; + + @Field(() => BuyerInfoComponent, { nullable: true }) + contactInfo?: BuyerInfoComponent; + + @Field(() => BuyerInfoComponent, { nullable: true }) + billingInfo?: BuyerInfoComponent; + + /* + PROMOTIONS + */ + + @Field(() => ID, { nullable: true }) + promoId?: string; + + /* + INVOICING + */ + + @Field(() => InvoiceInfo, { nullable: true }) + @Type(() => InvoiceInfo) + invoiceInfo?: InvoiceInfo; + + /* + PROVIDERS + */ + @Field(() => [ProviderSchema], { nullable: true }) + @Type(() => ProviderSchema) + providerOrderItems?: ProviderSchema[]; + + @Field(() => TranslatableComponent, { nullable: true }) + orderName?: TranslatableComponent; +} + +@ObjectType() +export class OrderEngineDBInterfaceSchema extends OrderEngineBaseSchema { + @Field(() => OrderStatusEnum) + orderStatus: OrderStatusEnum; + + @Field(() => PaymentIntentInfo) + paymentIntent: PaymentIntentInfo; + + @Field(() => PaymentStatusEnum, { nullable: true }) + paymentStatus?: PaymentStatusEnum; + @Field(() => PaymentInfo, { nullable: true }) + paymentInfo?: PaymentInfo; + + inputData?: any; +} + +@ObjectType() +export class OrderEngineDBSchema extends OrderEngineDBInterfaceSchema { + @Field(() => Number) + async subTotalPrice(@Ctx() ctx: ApolloContext): Promise { + return await getSubTotalPrice(this, ctx); + } + + @Field(() => Number) + async vatPrice(@Ctx() ctx: ApolloContext): Promise { + return await getVatPrice(this, ctx); + } + + @Field(() => Number) + async finalPrice(@Ctx() ctx: ApolloContext): Promise { + if (this.vatExempt) return getSubTotalPrice(this, ctx); + return await getTotalPrice(this, ctx); + } + + @Field(() => Number) + async totalPrice(@Ctx() ctx: ApolloContext): Promise { + return this.finalPrice(ctx) + } + + @Field(() => PromoModel, { nullable: true }) + async promo(@Ctx() ctx: ApolloContext): Promise { + if (this.promoId) return await ctx.ctx.loaders.promoLoader.load(this.promoId); + return; + } +} + +@ObjectType({ implements: IEngineSchema }) +export class OrderEngineSchema extends OrderEngineDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + paths?: EnginePathComponent[]; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/services/module-payments/functions/orders/schemas/provider.schema.ts b/services/module-payments/functions/orders/schemas/provider.schema.ts new file mode 100644 index 0000000..a4cb512 --- /dev/null +++ b/services/module-payments/functions/orders/schemas/provider.schema.ts @@ -0,0 +1,19 @@ +import { OrderBusinessStatusEnum } from '@services/module-payments/components/components.orders'; +import { LineItemSchema } from '@src/orders/components/line.schema'; +import { ObjectType, Field, InputType } from 'type-graphql'; + +@InputType() +@ObjectType() +export class ProviderSchema { + @Field() + _id: string; + + @Field() + organisationId: string; + + @Field(() => OrderBusinessStatusEnum) + orderStatus: OrderBusinessStatusEnum; + + @Field(() => [LineItemSchema]) + lines: LineItemSchema[]; +} diff --git a/services/module-payments/functions/orders/services/CartService.ts b/services/module-payments/functions/orders/services/CartService.ts new file mode 100644 index 0000000..4486fac --- /dev/null +++ b/services/module-payments/functions/orders/services/CartService.ts @@ -0,0 +1,113 @@ +import { ApolloContext } from '@lib/seed/interfaces/context'; +import { OrderStatusEnum } from '@services/module-payments/components/components.orders'; +import { PaymentIntentInfo } from '@services/module-payments/components/components.payments'; +import { updatePaymentIntent } from '@services/module-payments/helpers/payments.helpers'; +import { OrderNewInputSchema } from '@src/orders/components/components.cart'; +import { LineItemBaseSchema, LineItemDBSchema } from '@src/orders/components/line.schema'; +import OrderModel from '@src/orders/order.model'; +import { ApolloError } from 'apollo-server-lambda'; +import { plainToClass } from 'class-transformer'; +import _ from 'lodash'; +import { LineProductBase } from '../carts/schemas/line.schema'; +import OrderEngineModel from '../order.model'; +import { OrderEngineSchema } from '../schemas/order.schema'; +import { createDraftOrder } from './OrderService'; + +import { v4 as uuid } from 'uuid'; + +export const getCurrentCartOrder = async (ctx: ApolloContext, input?: OrderNewInputSchema): Promise => { + const account = ctx.ctx.user; + const orderModel = new OrderModel(); + + try { + try { + return await orderModel.getOne({ query: { orderStatus: OrderStatusEnum.draft, accountId: account._id } }); + } catch (error) { + const co: any = { + ctx, + }; + if (input?.localeInfo) { + co.localeInfo = input.localeInfo; + delete input.localeInfo; + } + co.input = input; + + return await createDraftOrder(co); + } + } catch (error) { + throw error; + } +}; + +export const addToCart = async (input: T, ctx: ApolloContext): Promise => { + const orderModel = new OrderEngineModel(); + const account = ctx.ctx.user; + + const { productRessource, productId, quantity, parentId, options } = input; + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + + // Check the lines and their options + const lineIndex = _.findIndex(currentCartOrder.lines, function(o) { + if (o.productId == productId) { + if (o.options && options) { + const from = JSON.parse(JSON.stringify(o.options)); + const to = JSON.parse(JSON.stringify(options)); + + return _.isEqual(from, to); + } else return true; + } + return false; + }); + + if (lineIndex === -1) { + // New Item + const _id = uuid(); + + // Verify if has parent ID + if (parentId) { + const parentIndex = _.findIndex(currentCartOrder.lines, { _id: parentId }); + if (parentIndex === -1) throw new ApolloError('parentProduct.notFound', '404'); + } + + // Verify if item exists + const productLine = plainToClass(LineItemDBSchema, { + _id, + ...input, + }); + + const product = await productLine.getProduct(ctx); + if (!product) throw new ApolloError('product.notFound', '404'); + + // Verify it's option + if (product.getLineProductFunctions().validateCartOption && input.options) + product.getLineProductFunctions().validateCartOption?.(input.options); + + const lineToAdd = productLine; //plainToClass(LineItemDBSchema, input); + currentCartOrder.lines.push(lineToAdd); + } else { + // Update the quantity + currentCartOrder.lines[lineIndex].quantity = quantity; + } + + const paymentIntentUpdated: PaymentIntentInfo = await updatePaymentIntent(currentCartOrder.paymentIntent, { + amount: await currentCartOrder.finalPrice(ctx), + currency: currentCartOrder.currency, + account, + }); + + return await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $set: { + paymentIntent: paymentIntentUpdated, + lines: currentCartOrder.lines, + }, + }, + ctx, + }); + } catch (error) { + throw error; + } +}; diff --git a/services/module-payments/functions/orders/services/LineService.ts b/services/module-payments/functions/orders/services/LineService.ts new file mode 100644 index 0000000..e69de29 diff --git a/services/module-payments/functions/orders/services/OrderService.ts b/services/module-payments/functions/orders/services/OrderService.ts new file mode 100644 index 0000000..52ffc16 --- /dev/null +++ b/services/module-payments/functions/orders/services/OrderService.ts @@ -0,0 +1,290 @@ +import { ApolloContext, ApolloContextLoadersOnly } from '@seed/interfaces/context'; +import { v4 as uuid } from 'uuid'; +import { ApolloError } from 'apollo-server-lambda'; +import _ from 'lodash'; +import { ProviderSchema } from '../schemas/provider.schema'; +import { OrderEngineDBSchema, OrderEngineSchema } from '../schemas/order.schema'; + +import { LocaleInfo, OrderBusinessStatusEnum, OrderStatusEnum } from '@services/module-payments/components/components.orders'; +import { CurrencyEnum, PaymentIntentInfo, PaymentStatusEnum, PaymentProvider } from '@services/module-payments/components/components.payments'; + +import { confirmPaymentIntent, createPaymentIntent } from '@services/module-payments/helpers/payments.helpers'; +import { CheckoutEngineInput } from '../schemas/order.schema.input'; + +import { LineProductInterface } from '../carts/schemas/line.schema'; +import { plainToClass } from 'class-transformer'; +import { getCurrentCartOrder } from './CartService'; +import { AvailableTranslation } from '@src/__components/components'; +import OrderModel from '@src/orders/order.model'; +import { LineItemSchema } from '@src/orders/components/line.schema'; +import { createApolloContext } from '@seed/graphql/Middleware'; +import { newError } from '@seed/helpers/Error'; +import OrderEngineModel from '../order.model'; +import { EnginePathComponent } from '@seed/interfaces/components'; + +interface ValidateCheckoutInput { + (input: OrderEngineSchema): Promise; +} + +export interface CheckoutInputInterface { + payment: PaymentProvider; + contactInfo: CheckoutEngineInput; + input: any; + ctx: ApolloContext; + + validateInputCheckout?: ValidateCheckoutInput; +} + +export interface finaliseCheckoutWithOrderInputInterface { + type: 'default' | 'event'; + payment: PaymentProvider; + contactInfo: CheckoutEngineInput; + input: any; + ctx: ApolloContext; +} + +export interface beginCheckoutWithOrderInputInterface { + contactInfo?: CheckoutEngineInput; + lines: LineProductInterface[]; + input: any; + ctx: ApolloContext; + paths?: EnginePathComponent[]; +} + +export interface CheckoutInterface { + payment: PaymentProvider; + cartOrderData: OrderEngineSchema; + contactInfo: CheckoutEngineInput; + input: any; +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const setProviderData = async (orderData: OrderEngineSchema, ctx: ApolloContext, providersData: ProviderSchema[]) => { + for (let index = 0; index < orderData.lines.length; index++) { + const element = orderData.lines[index]; + const product = await element.getProduct(ctx); + + if (product?.organisationId) { + const index = _.findIndex(providersData, { organisationId: product.organisationId }); + if (index === -1) + providersData.push({ + _id: uuid(), + orderStatus: OrderBusinessStatusEnum.processing, + organisationId: product.organisationId, + lines: [element], + }); + else { + providersData[index].lines.push(element); + } + } + } +}; + +export const createOrder = async (data: { + lines?: LineProductInterface[]; + status?: OrderStatusEnum; + contactInfo?: CheckoutEngineInput; + currency?: CurrencyEnum; + localeInfo?: LocaleInfo; + input?: any; + paths?: EnginePathComponent[]; + ctx: ApolloContext; +}) => { + let { contactInfo, lines, currency, localeInfo, status, input, paths, ctx } = data; + const accountId = ctx.ctx.user._id; + + const currentLines = lines || []; + if (!currency) currency = (process.env.PAYMENT_DEFAULT_CURRENCY as CurrencyEnum) || CurrencyEnum.usd; + if (!status) status = OrderStatusEnum.draft; + if (!localeInfo) + localeInfo = { + countryCode: process.env.PAYMENT_DEFAULT_COUNTRY || 'BE', + locale: (process.env.PAYMENT_DEFAULT_LOCALE as any) || AvailableTranslation.en, + }; + + currentLines.forEach((line,key) => { + currentLines[key].localeInfo = localeInfo; + }); + + + + // (1) Create the order Info + const orderModel = new OrderModel({ + orderStatus: status, + accountId, + paymentIntent: { + provider: PaymentProvider.stripe, + }, + currency, + lines: plainToClass(LineItemSchema, currentLines), + localeInfo: localeInfo, + inputData: input, + + vatExempt: false, + + + ...contactInfo, + }); + + // (2) Get the amount & intent + const amount = await orderModel.dbData.finalPrice(ctx); + const paymentIntent: PaymentIntentInfo | undefined = await createPaymentIntent({ amount, currency, account: ctx.ctx.user }); + + if (!paymentIntent) throw newError(6000); + else { + return await orderModel.saveOne({ + additionnalData: { + paths, + paymentIntent, + }, + ctx, + }); + } +}; + +export const createDraftOrder = async (data: { currency?: CurrencyEnum; localeInfo?: LocaleInfo; input?: any; ctx: ApolloContext }) => { + return createOrder({ + status: OrderStatusEnum.draft, + ...data, + }); +}; + +export const checkout = async (checkoutData: CheckoutInterface): Promise => { + const { payment, cartOrderData, contactInfo, input } = checkoutData; + try { + if (!cartOrderData.lines || cartOrderData.lines.length == 0) throw newError(6003); + + let provider, + transactionId, + paymentStatus: PaymentStatusEnum, + orderStatus: OrderStatusEnum = OrderStatusEnum.draft; + + if (payment == PaymentProvider.stripe) { + if (!cartOrderData.paymentIntent.stripePaymentIntentData) throw newError(6004); + paymentStatus = PaymentStatusEnum.pending; + orderStatus = OrderStatusEnum.processing; + provider = PaymentProvider.stripe; + transactionId = cartOrderData.paymentIntent.stripePaymentIntentData.id; + } else { + if (payment == PaymentProvider.free) { + provider = PaymentProvider.bankWire; + // TODO + transactionId = ''; + paymentStatus = PaymentStatusEnum.free; + orderStatus = OrderStatusEnum.processing; + } else { + provider = PaymentProvider.bankWire; + // TODO + transactionId = ''; + paymentStatus = PaymentStatusEnum.pending; + orderStatus = OrderStatusEnum.processing; + } + } + + // Update the order data + const orderModel = new OrderModel(); + + const dataToSave: Partial = { + orderStatus, + billingInfo: contactInfo.billingInfo, + contactInfo: contactInfo.contactInfo, + paymentStatus: paymentStatus, + paymentInfo: { + provider, + transactionId, + }, + inputData: input, + }; + + const result = await orderModel.updateOne({ + query: { _id: cartOrderData._id }, + newData: dataToSave, + }); + + if (payment != PaymentProvider.stripe) await orderModel.afterPaymentSuccess(result); + + return result; + } catch (error) { + throw error; + } +}; + +export const createCheckout = async (data: CheckoutInputInterface): Promise => { + try { + const { validateInputCheckout, payment, ctx, contactInfo, input } = data; + // Get the cart + const cartOrderData = await getCurrentCartOrder(ctx); + if (!cartOrderData) throw new ApolloError('cart.notFound', '404'); + + // Validate input function + if (validateInputCheckout) validateInputCheckout(cartOrderData); + + return await checkout({ payment, cartOrderData, contactInfo, input }); + } catch (error) { + throw error; + } +}; + +export const beginCheckoutWithOrder = async (data: beginCheckoutWithOrderInputInterface): Promise => { + try { + const { contactInfo, lines, ctx, input, paths } = data; + + // Delete all previous cart + await (await new OrderEngineModel().db()).deleteMany({ accountId: ctx.ctx.user._id }); + + // Get the cart + const cartOrderData = await createOrder({ + contactInfo, + lines, + input: input, + paths, + ctx, + }); + + if (!cartOrderData) throw new ApolloError('cart.notFound', '404'); + + return cartOrderData; + } catch (error) { + throw error; + } +}; + +export const finaliseCheckoutWithOrder = async (data: finaliseCheckoutWithOrderInputInterface): Promise => { + try { + const { type, payment, contactInfo, ctx, input } = data; + + // Get the cart + const cartOrderData = await getCurrentCartOrder(ctx); + if (!cartOrderData) throw new ApolloError('cart.notFound', '404'); + + return await checkout({ payment, cartOrderData, contactInfo, input }); + } catch (error) { + throw error; + } +}; + +export async function localCheckout(orderData: OrderEngineSchema): Promise { + if (process.env.NODE_ENV === 'local') { + const orderModel = new OrderModel(); + try { + // Update the payment Intent and it's status + const dataToSave: Partial = { + paymentStatus: PaymentStatusEnum.paid, + orderStatus: OrderStatusEnum.processing, + }; + // check if organisations ids on product + const providersData: ProviderSchema[] = []; + await setProviderData(orderData, await createApolloContext(), providersData); + if (providersData.length > 0) dataToSave.providerOrderItems = providersData; + const order = await orderModel.updateOne({ + query: { _id: orderData._id }, + newData: dataToSave, + }); + await confirmPaymentIntent(orderData.paymentIntent); + await orderModel.afterPaymentSuccess(order); + } catch (error) { + console.error(error); + } + } + else throw newError(1002) +} diff --git a/services/module-payments/functions/paymentMethods/pm.resolver.admin.ts b/services/module-payments/functions/paymentMethods/pm.resolver.admin.ts new file mode 100644 index 0000000..a704264 --- /dev/null +++ b/services/module-payments/functions/paymentMethods/pm.resolver.admin.ts @@ -0,0 +1,14 @@ +import AccountModel from '@src/accounts/account.model'; +import { Resolver } from 'type-graphql'; + +@Resolver(AccountModel) +export default class AccountPaymentMethodAdminResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-payments/functions/paymentMethods/pm.resolver.fields.ts b/services/module-payments/functions/paymentMethods/pm.resolver.fields.ts new file mode 100644 index 0000000..c615ef4 --- /dev/null +++ b/services/module-payments/functions/paymentMethods/pm.resolver.fields.ts @@ -0,0 +1,14 @@ +import AccountModel from '@src/accounts/account.model'; +import { Resolver } from 'type-graphql'; + +@Resolver(AccountModel) +export default class AccountPaymentMethodFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-payments/functions/paymentMethods/pm.resolver.ts b/services/module-payments/functions/paymentMethods/pm.resolver.ts new file mode 100644 index 0000000..77b150a --- /dev/null +++ b/services/module-payments/functions/paymentMethods/pm.resolver.ts @@ -0,0 +1,95 @@ +import { Resolver, Arg, Mutation, Ctx, Authorized } from 'type-graphql'; +import AccountModel from '@src/accounts/account.model'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import StripeService from '@services/module-payments/services/stripe/StripeService'; +import { ApolloError } from 'apollo-server-lambda'; +import { BillingAddressComponent } from '@services/module-payments/components/components'; +import { PaymentMethodDetail, PaymentMethodInput } from '@services/module-payments/components/components.payments'; +import { newError } from '@seed/helpers/Error'; + +@Resolver(AccountModel) +export default class AccountPaymentMethodResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => PaymentMethodDetail) + @Authorized() + async accountsPaymentMethodsAddOne(@Arg('input') input: PaymentMethodInput, @Ctx() ctx: ApolloContext): Promise { + const model = ctx.ctx.user; + + // Get the card information from stripe + const pmResult = await StripeService.getInstance().createPaymentMethod(model, input.sPayMethodId); + + const cardDetail = { + ...input, + sPayMethodId: pmResult.id, + type: pmResult.type, + cardInfo: pmResult.card, + }; + + if (input.default && model.paymentInfo && model.paymentInfo.paymentMethods) { + // Remove all default from others + await model.updateOneCustom( + { _id: model._id }, + { + $set: { 'paymentInfo.paymentMethods.$[].default': false }, + }, + ctx, + ); + } else input.default = false; + + return await model.addOneSubRessource({ _id: model._id }, 'paymentInfo.paymentMethods', cardDetail, ctx); + } + + @Mutation(() => PaymentMethodDetail) + @Authorized() + async accountsPaymentMethodsMarkasDefault(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = ctx.ctx.user; + + // Remove all default from others + await model.updateOneCustom( + { _id: model._id, 'paymentInfo.paymentMethods._id': id }, + { + $set: { 'paymentInfo.paymentMethods.$[].default': false }, + }, + ctx, + ); + + await model.updateOneCustom( + { _id: model._id, 'paymentInfo.paymentMethods._id': id }, + { + $set: { 'paymentInfo.paymentMethods.$.default': true }, + }, + ctx, + ); + + return await model.getOneSubRessource('paymentInfo.paymentMethods', id); + } + + @Mutation(() => String) + @Authorized() + async accountsPaymentMethodsDeleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = ctx.ctx.user; + + // Get the PM id + const currentPM = model.getOneSubRessource('paymentInfo.paymentMethods', id) as PaymentMethodDetail; + if (!currentPM) throw newError(404); + + // Get the card information from stripe + return await model.deleteOneSubRessource({ _id: model._id }, 'paymentInfo.paymentMethods', id, ctx); + } +} diff --git a/services/module-payments/functions/payoutAccounts/pa.resolver.admin.ts b/services/module-payments/functions/payoutAccounts/pa.resolver.admin.ts new file mode 100644 index 0000000..7ce45ce --- /dev/null +++ b/services/module-payments/functions/payoutAccounts/pa.resolver.admin.ts @@ -0,0 +1,14 @@ +import AccountModel from '@src/accounts/account.model'; +import { Resolver } from 'type-graphql'; + +@Resolver(AccountModel) +export default class AccountPayoutAccountsAdminResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-payments/functions/payoutAccounts/pa.resolver.fields.ts b/services/module-payments/functions/payoutAccounts/pa.resolver.fields.ts new file mode 100644 index 0000000..1832899 --- /dev/null +++ b/services/module-payments/functions/payoutAccounts/pa.resolver.fields.ts @@ -0,0 +1,14 @@ +import AccountModel from '@src/accounts/account.model'; +import { Resolver } from 'type-graphql'; + +@Resolver(AccountModel) +export default class AccountPayoutAccountsFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ +} diff --git a/services/module-payments/functions/payoutAccounts/pa.resolver.ts b/services/module-payments/functions/payoutAccounts/pa.resolver.ts new file mode 100644 index 0000000..186d3af --- /dev/null +++ b/services/module-payments/functions/payoutAccounts/pa.resolver.ts @@ -0,0 +1,61 @@ +import { Resolver, Arg, Mutation, Ctx, Authorized, Query, Args } from 'type-graphql'; +import AccountModel from '@src/accounts/account.model'; +import { ApolloContext } from '@lib/seed/interfaces/context'; +import StripeService from '@services/module-payments/services/stripe/StripeService'; + +@Resolver(AccountModel) +export default class AccountPayoutAccountsResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => String) + @Authorized() + async accountsDashboardLinkStripe(@Arg('returnPath', {nullable:true}) returnPath:string | undefined, @Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + + if (account.payoutInfo?.stripeInfo?.detailsSubmitted) { + // Get dashboard link + return await StripeService.getInstance().createAccountDashboardLink(account); + } + // Get Create Link + else return (await StripeService.getInstance().createAccountLink(account, returnPath, 'create')).url; + } + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => String) + @Authorized() + async accountsLinkWithStripe(@Arg('returnPath', {nullable:true}) returnPath:string | undefined, @Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + + // Check if update of create of the link + let type: 'create' | 'update' = 'create'; + if (account.payoutInfo?.stripeInfo?.detailsSubmitted) type = 'update'; + + // Get the card information from stripe + const stripeAccountLink = await StripeService.getInstance().createAccountLink(account, returnPath, type); + + return stripeAccountLink.url; + } + + @Mutation(() => AccountModel) + @Authorized() + async accountsRefreshInfoStripe(@Ctx() ctx: ApolloContext): Promise { + const account = ctx.ctx.user; + + // Get the card information from stripe + return await StripeService.getInstance().refreshAccountInfo(account); + } +} diff --git a/services/module-payments/functions/promos/component.ts b/services/module-payments/functions/promos/component.ts new file mode 100644 index 0000000..58b80b9 --- /dev/null +++ b/services/module-payments/functions/promos/component.ts @@ -0,0 +1,9 @@ +import { registerEnumType } from 'type-graphql'; + +export enum PromoType { + fixed = 'fixed', + percentage = 'percentage', +} +registerEnumType(PromoType, { + name: 'PromoType', +}); diff --git a/services/module-payments/functions/promos/promo.model.ts b/services/module-payments/functions/promos/promo.model.ts new file mode 100644 index 0000000..a6ab3e0 --- /dev/null +++ b/services/module-payments/functions/promos/promo.model.ts @@ -0,0 +1,171 @@ +import { ObjectType, Field, ID, ArgsType, InputType, registerEnumType } from 'type-graphql'; +import { ApolloError } from 'apollo-server-lambda'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetArgs } from '@seed/graphql/Request'; + +import AccountModel from '@src/accounts/account.model'; +import { TranslatableComponent } from '@src/__components/components'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ApolloContext } from '@seed/interfaces/context'; +import { PromoType } from './component'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public, AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class PromoModel extends BaseGraphModel { + public constructor() { + super({ + collectionName: 'shop.promos', + permissions: permissions, + }); + } + @Field(() => ID) + readonly _id: string; + + @Field() + code: string; + + @Field(() => TranslatableComponent, { nullable: true }) + description: TranslatableComponent; + + @Field(() => PromoType) + type: PromoType; + + @Field() + value: number; + + @Field({ nullable: true }) + validity?: Date; + + @Field() + cummulable: boolean; + + @Field({ nullable: true }) + usageLimit?: number; + + @Field() + usage: number; + + searchOptions(): string[] { + return ['code']; + } + + filterOptions(): string[] { + return ['code']; + } + + // validatePromo = (): void => { + // try { + // if (this.usageLimit && this.usageLimit <= this.usage) { + // throw new ApolloError('promo.notValid', '404'); + // } + // if (this.validity) { + // const today = new Date(); + // if (this.validity < today) throw new ApolloError('promo.expired', '404'); + // } + // } catch (error) { + // throw error; + // } + // }; +} + +@ArgsType() +export class PromoArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; + @Field({ nullable: true }) + search?: string; +} + +@InputType() +export class NewPromoInput { + @Field() + code: string; + + @Field(() => TranslatableComponent, { nullable: true }) + description: TranslatableComponent; + + @Field(() => PromoType) + type: PromoType; + + @Field() + value: number; + + @Field() + cummulable: boolean; + + @Field({ nullable: true }) + validity?: Date; + + @Field({ nullable: true }) + usageLimit?: number; +} + +@InputType() +export class EditPromoInput { + @Field({ nullable: true }) + code: string; + + @Field(() => TranslatableComponent, { nullable: true }) + description: TranslatableComponent; + + @Field(() => PromoType, { nullable: true }) + type: PromoType; + + @Field({ nullable: true }) + value: number; + + @Field({ nullable: true }) + cummulable: boolean; + + @Field({ nullable: true }) + validity?: Date; + + @Field({ nullable: true }) + usageLimit?: number; +} + +export const onCreate = async (input: NewPromoInput, ctx: ApolloContext): Promise => { + try { + const promo = await (await new PromoModel().db()).findOne({ code: input.code }); + if (promo) { + throw new ApolloError('promo.nameTaken', '404'); + } + } catch (error) { + throw error; + } +}; + +export const onUpdate = async (input: EditPromoInput, ctx: ApolloContext): Promise => { + try { + const promo = await (await new PromoModel().db()).findOne({ code: input.code }); + if (promo) { + throw new ApolloError('promo.nameTaken', '404'); + } + } catch (error) { + throw error; + } +}; + +export const validatePromo = async (code: string, ctx: ApolloContext): Promise => { + try { + const promo = await new PromoModel().getOne({ code }, ctx); + if (promo.usageLimit && promo.usageLimit <= promo.usage) { + throw new ApolloError('promo.notValid', '404'); + } + if (promo.validity) { + const today = new Date(); + if (promo.validity < today) throw new ApolloError('promo.expired', '404'); + } + } catch (error) { + throw error; + } +}; diff --git a/services/module-payments/functions/promos/promo.resolver.admin.ts b/services/module-payments/functions/promos/promo.resolver.admin.ts new file mode 100644 index 0000000..501c597 --- /dev/null +++ b/services/module-payments/functions/promos/promo.resolver.admin.ts @@ -0,0 +1,51 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; + +import PromoModel, { PromoArgs, NewPromoInput, EditPromoInput } from '@services/module-payments/functions/promos/promo.model'; +import { ApolloContext } from '@seed/interfaces/context'; +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric } from '@seed/graphql/BaseService'; +import { onCreate, onUpdate } from './promo.model'; + +const PromoBaseResolver = createBaseResolver('promos', PromoModel, PromoArgs, NewPromoInput, EditPromoInput, [AccountTypeEnum.admin]); + +@Resolver(PromoModel) +export default class PromoAdminResolver extends PromoBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => PromoModel) + @Authorized([AccountTypeEnum.admin]) + async addOne(@Arg('input') input: NewPromoInput, @Ctx() ctx: ApolloContext): Promise { + const model = new PromoModel(); + const toSave = { + ...input, + usage: 0, + }; + await onCreate(input, ctx); + + return addOneGeneric(model, toSave, ctx); + } + + @Mutation(() => PromoModel) + @Authorized([AccountTypeEnum.admin]) + async editOne(@Arg('id') id: string, @Arg('input') input: EditPromoInput, @Ctx() ctx: ApolloContext): Promise { + const model = new PromoModel(); + await onUpdate(input, ctx); + + return editOneGeneric(model, id, input, ctx); + } +} diff --git a/services/module-payments/functions/promos/promo.resolver.ts b/services/module-payments/functions/promos/promo.resolver.ts new file mode 100644 index 0000000..2d9147e --- /dev/null +++ b/services/module-payments/functions/promos/promo.resolver.ts @@ -0,0 +1,32 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; + +import { getOneGeneric } from '@seed/graphql/BaseService'; +import PromoModel from '@services/module-payments/functions/promos/promo.model'; +import { ApolloContext } from '@seed/interfaces/context'; + +@Resolver(PromoModel) +export default class PromoResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => PromoModel) + async promosGetOneByCode(@Arg('code') code: string, @Ctx() ctx: ApolloContext): Promise { + const model = new PromoModel(); + return await model.getOne({ code }, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/services/module-payments/functions/vat/vat.components.ts b/services/module-payments/functions/vat/vat.components.ts new file mode 100644 index 0000000..4298758 --- /dev/null +++ b/services/module-payments/functions/vat/vat.components.ts @@ -0,0 +1,761 @@ +import { ObjectType, Field, InputType } from 'type-graphql'; + +@ObjectType() +@InputType('TVAValueInput') +export class TVAValue { + @Field(() => Number, { defaultValue: 21 }) + AF: number; + + @Field(() => Number, { defaultValue: 21 }) + AX: number; + + @Field(() => Number, { defaultValue: 21 }) + AL: number; + + @Field(() => Number, { defaultValue: 21 }) + DZ: number; + + @Field(() => Number, { defaultValue: 21 }) + AS: number; + + @Field(() => Number, { defaultValue: 21 }) + AD: number; + + @Field(() => Number, { defaultValue: 21 }) + AO: number; + + @Field(() => Number, { defaultValue: 21 }) + AI: number; + + @Field(() => Number, { defaultValue: 21 }) + AQ: number; + + @Field(() => Number, { defaultValue: 21 }) + AG: number; + + @Field(() => Number, { defaultValue: 21 }) + AR: number; + + @Field(() => Number, { defaultValue: 21 }) + AM: number; + + @Field(() => Number, { defaultValue: 21 }) + AW: number; + + @Field(() => Number, { defaultValue: 21 }) + AU: number; + + @Field(() => Number, { defaultValue: 21 }) + AT: number; + + @Field(() => Number, { defaultValue: 21 }) + AZ: number; + + @Field(() => Number, { defaultValue: 21 }) + BS: number; + + @Field(() => Number, { defaultValue: 21 }) + BH: number; + + @Field(() => Number, { defaultValue: 21 }) + BD: number; + + @Field(() => Number, { defaultValue: 21 }) + BB: number; + + @Field(() => Number, { defaultValue: 21 }) + BY: number; + + @Field(() => Number, { defaultValue: 21 }) + BE: number; + + @Field(() => Number, { defaultValue: 21 }) + BZ: number; + + @Field(() => Number, { defaultValue: 21 }) + BJ: number; + + @Field(() => Number, { defaultValue: 21 }) + BM: number; + + @Field(() => Number, { defaultValue: 21 }) + BT: number; + + @Field(() => Number, { defaultValue: 21 }) + BO: number; + + @Field(() => Number, { defaultValue: 21 }) + BA: number; + + @Field(() => Number, { defaultValue: 21 }) + BW: number; + + @Field(() => Number, { defaultValue: 21 }) + BV: number; + + @Field(() => Number, { defaultValue: 21 }) + BR: number; + + @Field(() => Number, { defaultValue: 21 }) + IO: number; + + @Field(() => Number, { defaultValue: 21 }) + BN: number; + + @Field(() => Number, { defaultValue: 21 }) + BG: number; + + @Field(() => Number, { defaultValue: 21 }) + BF: number; + + @Field(() => Number, { defaultValue: 21 }) + BI: number; + + @Field(() => Number, { defaultValue: 21 }) + KH: number; + + @Field(() => Number, { defaultValue: 21 }) + CM: number; + + @Field(() => Number, { defaultValue: 21 }) + CA: number; + + @Field(() => Number, { defaultValue: 21 }) + CV: number; + + @Field(() => Number, { defaultValue: 21 }) + KY: number; + + @Field(() => Number, { defaultValue: 21 }) + CF: number; + + @Field(() => Number, { defaultValue: 21 }) + TD: number; + + @Field(() => Number, { defaultValue: 21 }) + CL: number; + + @Field(() => Number, { defaultValue: 21 }) + CN: number; + + @Field(() => Number, { defaultValue: 21 }) + CX: number; + + @Field(() => Number, { defaultValue: 21 }) + CC: number; + + @Field(() => Number, { defaultValue: 21 }) + CO: number; + + @Field(() => Number, { defaultValue: 21 }) + KM: number; + + @Field(() => Number, { defaultValue: 21 }) + CG: number; + + @Field(() => Number, { defaultValue: 21 }) + CD: number; + + @Field(() => Number, { defaultValue: 21 }) + CK: number; + + @Field(() => Number, { defaultValue: 21 }) + CR: number; + + @Field(() => Number, { defaultValue: 21 }) + CI: number; + + @Field(() => Number, { defaultValue: 21 }) + HR: number; + + @Field(() => Number, { defaultValue: 21 }) + CU: number; + + @Field(() => Number, { defaultValue: 21 }) + CY: number; + + @Field(() => Number, { defaultValue: 21 }) + CZ: number; + + @Field(() => Number, { defaultValue: 21 }) + DK: number; + + @Field(() => Number, { defaultValue: 21 }) + DJ: number; + + @Field(() => Number, { defaultValue: 21 }) + DM: number; + + @Field(() => Number, { defaultValue: 21 }) + DO: number; + + @Field(() => Number, { defaultValue: 21 }) + EC: number; + + @Field(() => Number, { defaultValue: 21 }) + EG: number; + + @Field(() => Number, { defaultValue: 21 }) + SV: number; + + @Field(() => Number, { defaultValue: 21 }) + GQ: number; + + @Field(() => Number, { defaultValue: 21 }) + ER: number; + + @Field(() => Number, { defaultValue: 21 }) + EE: number; + + @Field(() => Number, { defaultValue: 21 }) + ET: number; + + @Field(() => Number, { defaultValue: 21 }) + FK: number; + + @Field(() => Number, { defaultValue: 21 }) + FO: number; + + @Field(() => Number, { defaultValue: 21 }) + FJ: number; + + @Field(() => Number, { defaultValue: 21 }) + FI: number; + + @Field(() => Number, { defaultValue: 21 }) + FR: number; + + @Field(() => Number, { defaultValue: 21 }) + GF: number; + + @Field(() => Number, { defaultValue: 21 }) + PF: number; + + @Field(() => Number, { defaultValue: 21 }) + TF: number; + + @Field(() => Number, { defaultValue: 21 }) + GA: number; + + @Field(() => Number, { defaultValue: 21 }) + GM: number; + + @Field(() => Number, { defaultValue: 21 }) + GE: number; + + @Field(() => Number, { defaultValue: 21 }) + DE: number; + + @Field(() => Number, { defaultValue: 21 }) + GH: number; + + @Field(() => Number, { defaultValue: 21 }) + GI: number; + + @Field(() => Number, { defaultValue: 21 }) + GR: number; + + @Field(() => Number, { defaultValue: 21 }) + GL: number; + + @Field(() => Number, { defaultValue: 21 }) + GD: number; + + @Field(() => Number, { defaultValue: 21 }) + GP: number; + + @Field(() => Number, { defaultValue: 21 }) + GU: number; + + @Field(() => Number, { defaultValue: 21 }) + GT: number; + + @Field(() => Number, { defaultValue: 21 }) + GG: number; + + @Field(() => Number, { defaultValue: 21 }) + GN: number; + + @Field(() => Number, { defaultValue: 21 }) + GW: number; + + @Field(() => Number, { defaultValue: 21 }) + GY: number; + + @Field(() => Number, { defaultValue: 21 }) + HT: number; + + @Field(() => Number, { defaultValue: 21 }) + HM: number; + + @Field(() => Number, { defaultValue: 21 }) + VA: number; + + @Field(() => Number, { defaultValue: 21 }) + HN: number; + + @Field(() => Number, { defaultValue: 21 }) + HK: number; + + @Field(() => Number, { defaultValue: 21 }) + HU: number; + + @Field(() => Number, { defaultValue: 21 }) + IS: number; + + @Field(() => Number, { defaultValue: 21 }) + IN: number; + + @Field(() => Number, { defaultValue: 21 }) + ID: number; + + @Field(() => Number, { defaultValue: 21 }) + IR: number; + + @Field(() => Number, { defaultValue: 21 }) + IQ: number; + + @Field(() => Number, { defaultValue: 21 }) + IE: number; + + @Field(() => Number, { defaultValue: 21 }) + IM: number; + + @Field(() => Number, { defaultValue: 21 }) + IL: number; + + @Field(() => Number, { defaultValue: 21 }) + IT: number; + + @Field(() => Number, { defaultValue: 21 }) + JM: number; + + @Field(() => Number, { defaultValue: 21 }) + JP: number; + + @Field(() => Number, { defaultValue: 21 }) + JE: number; + + @Field(() => Number, { defaultValue: 21 }) + JO: number; + + @Field(() => Number, { defaultValue: 21 }) + KZ: number; + + @Field(() => Number, { defaultValue: 21 }) + KE: number; + + @Field(() => Number, { defaultValue: 21 }) + KI: number; + + @Field(() => Number, { defaultValue: 21 }) + KP: number; + + @Field(() => Number, { defaultValue: 21 }) + KR: number; + + @Field(() => Number, { defaultValue: 21 }) + KW: number; + + @Field(() => Number, { defaultValue: 21 }) + KG: number; + + @Field(() => Number, { defaultValue: 21 }) + LA: number; + + @Field(() => Number, { defaultValue: 21 }) + LV: number; + + @Field(() => Number, { defaultValue: 21 }) + LB: number; + + @Field(() => Number, { defaultValue: 21 }) + LS: number; + + @Field(() => Number, { defaultValue: 21 }) + LR: number; + + @Field(() => Number, { defaultValue: 21 }) + LY: number; + + @Field(() => Number, { defaultValue: 21 }) + LI: number; + + @Field(() => Number, { defaultValue: 21 }) + LT: number; + + @Field(() => Number, { defaultValue: 21 }) + LU: number; + + @Field(() => Number, { defaultValue: 21 }) + MO: number; + + @Field(() => Number, { defaultValue: 21 }) + MK: number; + + @Field(() => Number, { defaultValue: 21 }) + MG: number; + + @Field(() => Number, { defaultValue: 21 }) + MW: number; + + @Field(() => Number, { defaultValue: 21 }) + MY: number; + + @Field(() => Number, { defaultValue: 21 }) + MV: number; + + @Field(() => Number, { defaultValue: 21 }) + ML: number; + + @Field(() => Number, { defaultValue: 21 }) + MT: number; + + @Field(() => Number, { defaultValue: 21 }) + MH: number; + + @Field(() => Number, { defaultValue: 21 }) + MQ: number; + + @Field(() => Number, { defaultValue: 21 }) + MR: number; + + @Field(() => Number, { defaultValue: 21 }) + MU: number; + + @Field(() => Number, { defaultValue: 21 }) + YT: number; + + @Field(() => Number, { defaultValue: 21 }) + MX: number; + + @Field(() => Number, { defaultValue: 21 }) + FM: number; + + @Field(() => Number, { defaultValue: 21 }) + MD: number; + + @Field(() => Number, { defaultValue: 21 }) + MC: number; + + @Field(() => Number, { defaultValue: 21 }) + MN: number; + + @Field(() => Number, { defaultValue: 21 }) + MS: number; + + @Field(() => Number, { defaultValue: 21 }) + MA: number; + + @Field(() => Number, { defaultValue: 21 }) + MZ: number; + + @Field(() => Number, { defaultValue: 21 }) + MM: number; + + @Field(() => Number, { defaultValue: 21 }) + NA: number; + + @Field(() => Number, { defaultValue: 21 }) + NR: number; + + @Field(() => Number, { defaultValue: 21 }) + NP: number; + + @Field(() => Number, { defaultValue: 21 }) + NL: number; + + @Field(() => Number, { defaultValue: 21 }) + AN: number; + + @Field(() => Number, { defaultValue: 21 }) + NC: number; + + @Field(() => Number, { defaultValue: 21 }) + NZ: number; + + @Field(() => Number, { defaultValue: 21 }) + NI: number; + + @Field(() => Number, { defaultValue: 21 }) + NE: number; + + @Field(() => Number, { defaultValue: 21 }) + NG: number; + + @Field(() => Number, { defaultValue: 21 }) + NU: number; + + @Field(() => Number, { defaultValue: 21 }) + NF: number; + + @Field(() => Number, { defaultValue: 21 }) + MP: number; + + @Field(() => Number, { defaultValue: 21 }) + NO: number; + + @Field(() => Number, { defaultValue: 21 }) + OM: number; + + @Field(() => Number, { defaultValue: 21 }) + PK: number; + + @Field(() => Number, { defaultValue: 21 }) + PW: number; + + @Field(() => Number, { defaultValue: 21 }) + PS: number; + + @Field(() => Number, { defaultValue: 21 }) + PA: number; + + @Field(() => Number, { defaultValue: 21 }) + PG: number; + + @Field(() => Number, { defaultValue: 21 }) + PY: number; + + @Field(() => Number, { defaultValue: 21 }) + PE: number; + + @Field(() => Number, { defaultValue: 21 }) + PH: number; + + @Field(() => Number, { defaultValue: 21 }) + PN: number; + + @Field(() => Number, { defaultValue: 21 }) + PL: number; + + @Field(() => Number, { defaultValue: 21 }) + PT: number; + + @Field(() => Number, { defaultValue: 21 }) + PR: number; + + @Field(() => Number, { defaultValue: 21 }) + QA: number; + + @Field(() => Number, { defaultValue: 21 }) + RE: number; + + @Field(() => Number, { defaultValue: 21 }) + RO: number; + + @Field(() => Number, { defaultValue: 21 }) + RU: number; + + @Field(() => Number, { defaultValue: 21 }) + RW: number; + + @Field(() => Number, { defaultValue: 21 }) + SH: number; + + @Field(() => Number, { defaultValue: 21 }) + KN: number; + + @Field(() => Number, { defaultValue: 21 }) + LC: number; + + @Field(() => Number, { defaultValue: 21 }) + PM: number; + + @Field(() => Number, { defaultValue: 21 }) + VC: number; + + @Field(() => Number, { defaultValue: 21 }) + WS: number; + + @Field(() => Number, { defaultValue: 21 }) + SM: number; + + @Field(() => Number, { defaultValue: 21 }) + ST: number; + + @Field(() => Number, { defaultValue: 21 }) + SA: number; + + @Field(() => Number, { defaultValue: 21 }) + SN: number; + + @Field(() => Number, { defaultValue: 21 }) + CS: number; + + @Field(() => Number, { defaultValue: 21 }) + SC: number; + + @Field(() => Number, { defaultValue: 21 }) + SL: number; + + @Field(() => Number, { defaultValue: 21 }) + SG: number; + + @Field(() => Number, { defaultValue: 21 }) + SK: number; + + @Field(() => Number, { defaultValue: 21 }) + SI: number; + + @Field(() => Number, { defaultValue: 21 }) + SB: number; + + @Field(() => Number, { defaultValue: 21 }) + SO: number; + + @Field(() => Number, { defaultValue: 21 }) + ZA: number; + + @Field(() => Number, { defaultValue: 21 }) + GS: number; + + @Field(() => Number, { defaultValue: 21 }) + ES: number; + + @Field(() => Number, { defaultValue: 21 }) + LK: number; + + @Field(() => Number, { defaultValue: 21 }) + SD: number; + + @Field(() => Number, { defaultValue: 21 }) + SR: number; + + @Field(() => Number, { defaultValue: 21 }) + SJ: number; + + @Field(() => Number, { defaultValue: 21 }) + SZ: number; + + @Field(() => Number, { defaultValue: 21 }) + SE: number; + + @Field(() => Number, { defaultValue: 21 }) + CH: number; + + @Field(() => Number, { defaultValue: 21 }) + SY: number; + + @Field(() => Number, { defaultValue: 21 }) + TW: number; + + @Field(() => Number, { defaultValue: 21 }) + TJ: number; + + @Field(() => Number, { defaultValue: 21 }) + TZ: number; + + @Field(() => Number, { defaultValue: 21 }) + TH: number; + + @Field(() => Number, { defaultValue: 21 }) + TL: number; + + @Field(() => Number, { defaultValue: 21 }) + TG: number; + + @Field(() => Number, { defaultValue: 21 }) + TK: number; + + @Field(() => Number, { defaultValue: 21 }) + TO: number; + + @Field(() => Number, { defaultValue: 21 }) + TT: number; + + @Field(() => Number, { defaultValue: 21 }) + TN: number; + + @Field(() => Number, { defaultValue: 21 }) + TR: number; + + @Field(() => Number, { defaultValue: 21 }) + TM: number; + + @Field(() => Number, { defaultValue: 21 }) + TC: number; + + @Field(() => Number, { defaultValue: 21 }) + TV: number; + + @Field(() => Number, { defaultValue: 21 }) + UG: number; + + @Field(() => Number, { defaultValue: 21 }) + UA: number; + + @Field(() => Number, { defaultValue: 21 }) + AE: number; + + @Field(() => Number, { defaultValue: 21 }) + GB: number; + + @Field(() => Number, { defaultValue: 21 }) + US: number; + + @Field(() => Number, { defaultValue: 21 }) + UM: number; + + @Field(() => Number, { defaultValue: 21 }) + UY: number; + + @Field(() => Number, { defaultValue: 21 }) + UZ: number; + + @Field(() => Number, { defaultValue: 21 }) + VU: number; + + @Field(() => Number, { defaultValue: 21 }) + VE: number; + + @Field(() => Number, { defaultValue: 21 }) + VN: number; + + @Field(() => Number, { defaultValue: 21 }) + VG: number; + + @Field(() => Number, { defaultValue: 21 }) + VI: number; + + @Field(() => Number, { defaultValue: 21 }) + WF: number; + + @Field(() => Number, { defaultValue: 21 }) + EH: number; + + @Field(() => Number, { defaultValue: 21 }) + YE: number; + + @Field(() => Number, { defaultValue: 21 }) + ZM: number; + + @Field(() => Number, { defaultValue: 21 }) + ZW: number; +} + +@ObjectType() +export class VatResponse { + @Field(() => Boolean) + valid: boolean; + + @Field({ nullable: true }) + database?: string; + + @Field(() => Boolean) + format_valid?: boolean; + + @Field({ nullable: true }) + query?: string; + + @Field({ nullable: true }) + country_code?: string; + + @Field({ nullable: true }) + vat_number?: string; + + @Field({ nullable: true }) + company_name?: string; + + @Field({ nullable: true }) + company_address?: string; +} diff --git a/services/module-payments/functions/vat/vat.model.ts b/services/module-payments/functions/vat/vat.model.ts new file mode 100644 index 0000000..40acbe8 --- /dev/null +++ b/services/module-payments/functions/vat/vat.model.ts @@ -0,0 +1,104 @@ +import { ObjectType, Field, ID, ArgsType, InputType, registerEnumType, Ctx } from 'type-graphql'; + +import { BaseGraphModel } from '@seed/graphql/BaseGraphModel'; +import { Permission } from '@seed/interfaces/permission'; +import { TranslatableComponent } from '@src/__components/components'; +import { GetArgs } from '@seed/graphql/Request'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { ApolloContext } from '@seed/interfaces/context'; +import { TVAValue } from './vat.components'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin, AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class VatClassModel extends BaseGraphModel { + public constructor() { + super({ + // ...init, + collectionName: 'shop.vat', + permissions: permissions, + }); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @Field(() => ID) + readonly _id: string; + + @Field() + title: string; + + @Field(() => TVAValue) + values: TVAValue; + + searchOptions(): string[] { + return []; + } + + filterOptions(): string[] { + return []; + } +} + +@ArgsType() +export class VatClassArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@InputType() +export class EditVatClassInput { + @Field() + title: string; + + @Field(() => TVAValue, { nullable: true }) + values?: TVAValue; +} + +@InputType() +export class NewVatClassInput { + @Field() + title: string; + + @Field(() => TVAValue) + values: TVAValue; +} + +@InputType() +export class ValidateVatInput { + @Field() + countryCode: string; + + @Field() + vatNumber: string; +} + +@ObjectType() +export class VatResponse { + @Field(() => Boolean) + valid: boolean; + + @Field({ nullable: true }) + database?: string; + + @Field(() => Boolean) + format_valid?: boolean; + + @Field({ nullable: true }) + query?: string; + + @Field({ nullable: true }) + country_code?: string; + + @Field({ nullable: true }) + vat_number?: string; + + @Field({ nullable: true }) + company_name?: string; + + @Field({ nullable: true }) + company_address?: string; +} diff --git a/services/module-payments/functions/vat/vat.resolver.admin.ts b/services/module-payments/functions/vat/vat.resolver.admin.ts new file mode 100644 index 0000000..8ac65e8 --- /dev/null +++ b/services/module-payments/functions/vat/vat.resolver.admin.ts @@ -0,0 +1,71 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; +import { v4 as uuid } from 'uuid'; +import _ from 'lodash'; +import { FilterQuery } from 'mongodb'; +import { ApolloError } from 'apollo-server-lambda'; + +import VatClassModel, { VatClassArgs, NewVatClassInput, EditVatClassInput } from './vat.model'; +import { ApolloContext } from '@seed/interfaces/context'; +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { getOneGeneric, addOneGeneric, editOneGeneric, deleteOneGeneric, permissionsAddOne, permissionsRemoveOne } from '@seed/graphql/BaseService'; + +// const VatBaseResolver = createBaseResolver('Vat', VatModel, VatArgs, NewVatInput, EditVatInput, [AccountTypeEnum.admin]); + +@Resolver(VatClassModel) +export default class VatAdminResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => VatClassModel) + @Authorized([AccountTypeEnum.admin]) + async vatClassGetOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new VatClassModel(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [VatClassModel]) + @Authorized([AccountTypeEnum.admin]) + async vatClassGetMany(@Args() args: VatClassArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new VatClassModel(); + const q: FilterQuery = {}; + + return model.getMany(q, args.pagination, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => VatClassModel) + @Authorized([AccountTypeEnum.admin]) + async vatClassAddOne(@Arg('input') input: NewVatClassInput, @Ctx() ctx: ApolloContext): Promise { + const model = new VatClassModel(); + return addOneGeneric(model, input, ctx); + } + + @Mutation(() => VatClassModel) + @Authorized([AccountTypeEnum.admin]) + async vatClassEditOne(@Arg('id') id: string, @Arg('input') input: EditVatClassInput, @Ctx() ctx: ApolloContext): Promise { + const model = new VatClassModel(); + return editOneGeneric(model, id, input, ctx); + } + + @Mutation(() => VatClassModel) + @Authorized([AccountTypeEnum.admin]) + async vatClassDeleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new VatClassModel(); + return deleteOneGeneric(model, id, ctx); + } +} diff --git a/services/module-payments/functions/vat/vat.resolver.ts b/services/module-payments/functions/vat/vat.resolver.ts new file mode 100644 index 0000000..8148608 --- /dev/null +++ b/services/module-payments/functions/vat/vat.resolver.ts @@ -0,0 +1,89 @@ +import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; + +import VatClassModel from './vat.model'; +import OrderEngineModel from '@services/module-payments/functions/orders/order.model'; +import { ApolloContext } from '@seed/interfaces/context'; +import { ValidateVatInput, VatResponse } from './vat.model'; +import axios from 'axios'; +import { updatePaymentIntent } from '@services/module-payments/helpers/payments.helpers'; +import { PaymentIntentInfo } from '@services/module-payments/components/components.payments'; +import { getCurrentCartOrder } from '../orders/services/CartService'; + +@Resolver(VatClassModel) +export default class VatClassResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + @Mutation(() => Boolean) + @Authorized() + async vatValidate(@Arg('input') input: ValidateVatInput, @Ctx() ctx: ApolloContext): Promise { + //VATLAYER + const baseUrl = 'http://apilayer.net/api/validate?'; + const account = ctx.ctx.user; + const orderModel = new OrderEngineModel(); + + try { + const currentCartOrder = await getCurrentCartOrder(ctx); + + let valid = false; + let vatExempt = false; + + if (input.countryCode === process.env.SHOP_COUNTRY_CODE) valid = true; + else { + const vatResponse = await axios.get( + `${baseUrl}access_key=${process.env.VATLAYER_API_KEY}&vat_number=${input.countryCode}${input.vatNumber}`, + ); + if (vatResponse.data.valid) { + vatExempt = true; + valid = true; + } + } + + //Pas top de faire 2 requetes + + await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $set: { + vatExempt: vatExempt, + }, + }, + ctx, + }); + + //ne marche pas on dirait + const paymentIntentUpdated: PaymentIntentInfo = await updatePaymentIntent(currentCartOrder.paymentIntent, { + amount: await currentCartOrder.finalPrice(ctx), + currency: currentCartOrder.currency, + account, + }); + + await orderModel.updateOneCustom({ + query: { _id: currentCartOrder._id }, + updateRequest: { + $set: { + paymentIntent: paymentIntentUpdated, + }, + }, + ctx, + }); + + return valid; + } catch (error) { + throw error; + } + } +} diff --git a/services/module-payments/helpers/payments.helpers.ts b/services/module-payments/helpers/payments.helpers.ts new file mode 100644 index 0000000..fcf53b9 --- /dev/null +++ b/services/module-payments/helpers/payments.helpers.ts @@ -0,0 +1,93 @@ +import AccountModel from '@src/accounts/account.model'; +import { CurrencyEnum, PaymentIntentInfo, PaymentProvider } from '../components/components.payments'; +import StripeService from '../services/stripe/StripeService'; + +export const createPaymentIntent = async (input: { + amount?: number; + currency?: CurrencyEnum; + account?: AccountModel; +}): Promise => { + const { amount, currency, account } = input; + const currentPaymentProvider = process.env.PAYMENT_PROVIDER || 'stripe'; + + let paymentIntent: PaymentIntentInfo | undefined; + switch (currentPaymentProvider) { + case 'stripe': + const inputData: any = { + currency: currency || (process.env.PAYMENT_DEFAULT_CURRENCY as CurrencyEnum) || CurrencyEnum.usd, + }; + + if (amount) inputData.amount = amount; + if (account) inputData.account = account; + + // Create the PaymentIntent + const stripePaymentInfo = await StripeService.getInstance().createPaymentIntent(inputData); + + paymentIntent = { + provider: PaymentProvider.stripe, + stripePaymentIntentData: stripePaymentInfo as any, + }; + break; + + default: + break; + } + return paymentIntent; +}; + +export const updatePaymentIntent = async ( + paymentIntent: PaymentIntentInfo, + updateInfo: { + amount?: number; + currency?: CurrencyEnum; + account?: AccountModel; + }, +): Promise => { + const currentPaymentProvider = process.env.PAYMENT_PROVIDER || 'stripe'; + + switch (currentPaymentProvider) { + case 'stripe': + // Update the payment Intent + + if (paymentIntent.stripePaymentIntentData) { + const stripePaymentInfo = await StripeService.getInstance().updatePaymentIntent(paymentIntent.stripePaymentIntentData.id, updateInfo); + + paymentIntent = { + provider: PaymentProvider.stripe, + stripePaymentIntentData: stripePaymentInfo as any, + }; + } + + break; + + default: + break; + } + return paymentIntent; +}; + +export const confirmPaymentIntent = async (paymentIntent: PaymentIntentInfo): Promise => { + const currentPaymentProvider = process.env.PAYMENT_PROVIDER || 'stripe'; + + switch (currentPaymentProvider) { + case 'stripe': + // Update the payment Intent + + if (paymentIntent.stripePaymentIntentData) { + const stripePaymentInfo = await StripeService.getInstance().confirmPaymentIntent(paymentIntent.stripePaymentIntentData.id, { + paymentMethod: 'pm_card_visa', + }); + + paymentIntent = { + provider: PaymentProvider.stripe, + stripePaymentIntentData: stripePaymentInfo as any, + }; + } + + break; + + default: + break; + } + // return paymentIntent; +}; diff --git a/services/module-payments/index.ts b/services/module-payments/index.ts new file mode 100644 index 0000000..7f158bb --- /dev/null +++ b/services/module-payments/index.ts @@ -0,0 +1,9 @@ +import 'reflect-metadata'; +import AccountBillingInfoResolver from '@services/module-payments/functions/billingInfos/billingInfo.resolver'; +import AccountPaymentMethodResolver from '@services/module-payments/functions/paymentMethods/pm.resolver'; +import AccountPayoutAccountsResolver from '@services/module-payments/functions/payoutAccounts/pa.resolver'; + +export const PaymentAppEngineResolvers = [AccountBillingInfoResolver, AccountPaymentMethodResolver]; +export const PaymentAdminEngineResolvers = []; + +export const PayoutAppEngineResolvers = [AccountPayoutAccountsResolver]; diff --git a/services/module-payments/serverless-payments.yaml b/services/module-payments/serverless-payments.yaml new file mode 100644 index 0000000..20c2d67 --- /dev/null +++ b/services/module-payments/serverless-payments.yaml @@ -0,0 +1,46 @@ +app: ${file(./package.json):name} +service: payments +package: + individually: true +provider: + name: aws + stage: ${opt:stage,'dev'} + runtime: nodejs12.x + environment: + AWS_Hooks: hooks-${self:provider.stage}-hookHandler + iamRoleStatements: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: '*' +plugins: + - serverless-webpack + - serverless-domain-manager + - serverless-offline +custom: + stage: ${opt:stage, self:provider.stage} + domains: ${file(./config/domains.yaml):domains} + customDomain: ${file(./config/domains.yaml):customDomain} + serverless-offline: + httpPort: 4002 + lambdaPort: 4010 + webpack: + webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file + includeModules: + forceExclude: + - aws-sdk + - puppeteer +functions: + stripe-hooks: + handler: handler.handler + timeout: 300 + events: + - http: + path: '/webhook' + method: post + cors: + origin: '*' + headers: + - Content-Type + - Authorization + - stripe-signature diff --git a/services/module-payments/services/stripe/StripeService.ts b/services/module-payments/services/stripe/StripeService.ts new file mode 100644 index 0000000..99ad210 --- /dev/null +++ b/services/module-payments/services/stripe/StripeService.ts @@ -0,0 +1,382 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import Stripe from 'stripe'; +import { ApolloError } from 'apollo-server-lambda'; +import AccountModel from '@src/accounts/account.model'; +import _ from 'lodash'; +import { updateStripePayoutInfo } from './paymentIntent.helper'; +import { CurrencyEnum } from '@services/module-payments/components/components.payments'; +import { newError } from '@seed/helpers/Error'; + +export default class StripeService { + private static instance: StripeService; + public stripe: Stripe; + + private constructor() { + try { + this.stripe = new Stripe(process.env.STRIPE_SKEY || '', { maxNetworkRetries: 3, apiVersion: '2020-08-27' }); + } catch (error) { + console.error('STRIPE initialization error raised', error.stack); + } + } + + public static getInstance(): StripeService { + if (!StripeService.instance) { + StripeService.instance = new StripeService(); + } + return StripeService.instance; + } + + /* + ██████╗██╗ ██╗███████╗████████╗ ██████╗ ███╗ ███╗███████╗██████╗ ███████╗ + ██╔════╝██║ ██║██╔════╝╚══██╔══╝██╔═══██╗████╗ ████║██╔════╝██╔══██╗██╔════╝ + ██║ ██║ ██║███████╗ ██║ ██║ ██║██╔████╔██║█████╗ ██████╔╝███████╗ + ██║ ██║ ██║╚════██║ ██║ ██║ ██║██║╚██╔╝██║██╔══╝ ██╔══██╗╚════██║ + ╚██████╗╚██████╔╝███████║ ██║ ╚██████╔╝██║ ╚═╝ ██║███████╗██║ ██║███████║ + ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝ + */ + + public createCustomer = async (account: AccountModel): Promise => { + if (account.paymentInfo && account.paymentInfo.stripeInfo) return account.paymentInfo.stripeInfo.customerId; + + try { + const stripeCreateCustomer = await this.stripe.customers.create({ + email: account.email, + metadata: { + _id: account._id, + }, + name: account.firstName + ' ' + account.lastName, + }); + if (stripeCreateCustomer.id) { + await account.updateOneCustom({ _id: account._id }, { $set: { 'paymentInfo.stripeInfo.customerId': stripeCreateCustomer.id } }, null); + return stripeCreateCustomer.id; + } else throw stripeCreateCustomer; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + /* + █████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███╗ ██╗████████╗███████╗ + ██╔══██╗██╔════╝██╔════╝██╔═══██╗██║ ██║████╗ ██║╚══██╔══╝██╔════╝ + ███████║██║ ██║ ██║ ██║██║ ██║██╔██╗ ██║ ██║ ███████╗ + ██╔══██║██║ ██║ ██║ ██║██║ ██║██║╚██╗██║ ██║ ╚════██║ + ██║ ██║╚██████╗╚██████╗╚██████╔╝╚██████╔╝██║ ╚████║ ██║ ███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + + */ + + public createAccount = async (account: AccountModel): Promise => { + if (account.payoutInfo?.stripeInfo) return account; + + try { + const stripeCreateAccount = await this.stripe.accounts.create({ + type: 'express', + email: account.email, + }); + if (stripeCreateAccount.id) { + return await updateStripePayoutInfo(account, stripeCreateAccount); + } else throw stripeCreateAccount; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + public refreshAccountInfo = async (account: AccountModel): Promise => { + if (!account.payoutInfo?.stripeInfo?.accountId) throw newError(6001); + + try { + const stripeAccount = await this.stripe.accounts.retrieve(account.payoutInfo?.stripeInfo?.accountId); + if (stripeAccount.id) { + return await updateStripePayoutInfo(account, stripeAccount); + } else throw stripeAccount; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + public createAccountLink = async (account: AccountModel, returnPath: string | undefined, reason?: 'create' | 'update'): Promise => { + try { + const stripeAccount = await this.createAccount(account); + + if (stripeAccount.payoutInfo?.stripeInfo?.accountId) { + let type: Stripe.AccountLinkCreateParams.Type = 'account_onboarding'; + if (reason && reason == 'update') type = 'account_update'; + + let returnUrl = process.env.STRIPE_ACCOUNT_RETURN_URL || ''; + if (returnPath && returnPath != "") { + while(returnPath.charAt(0) === '/') + { + returnPath = returnPath.substring(1); + } + + returnUrl = process.env.STRIPE_ACCOUNT_REFRESH_URL + "/" + returnPath + } + + return await this.stripe.accountLinks.create({ + account: stripeAccount.payoutInfo?.stripeInfo?.accountId, + refresh_url: process.env.STRIPE_ACCOUNT_REFRESH_URL || '', + return_url: returnUrl, + type: type, + }); + } else throw newError(6001); + } catch (error) { + throw error; + } + }; + + public createAccountDashboardLink = async (account: AccountModel): Promise => { + try { + const stripeAccount = await this.createAccount(account); + + if (stripeAccount.payoutInfo?.stripeInfo?.accountId) { + return await (await this.stripe.accounts.createLoginLink(stripeAccount.payoutInfo?.stripeInfo?.accountId)).url; + } else throw newError(6001); + } catch (error) { + throw error; + } + }; + + /* + ███╗ ███╗███████╗████████╗██╗ ██╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██╔════╝╚══██╔══╝██║ ██║██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║█████╗ ██║ ███████║██║ ██║██║ ██║███████╗ + ██║╚██╔╝██║██╔══╝ ██║ ██╔══██║██║ ██║██║ ██║╚════██║ + ██║ ╚═╝ ██║███████╗ ██║ ██║ ██║╚██████╔╝██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ + */ + + public createPaymentMethod = async (account: AccountModel, paymentMethodId: string): Promise => { + let customerId; + // Check if customer exists + if (account.paymentInfo && account.paymentInfo.stripeInfo) { + customerId = account.paymentInfo.stripeInfo.customerId; + } else customerId = await this.createCustomer(account); + + try { + // Check if that card exists already + + const paymentMethod = await this.stripe.paymentMethods.attach(paymentMethodId, { + customer: customerId, + }); + + return paymentMethod; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + public deletePaymentMethod = async (paymentMethodId: string): Promise => { + try { + // Check if that card exists already + + const paymentMethod = await this.stripe.paymentMethods.detach(paymentMethodId); + + return paymentMethod; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + /* + ██╗███╗ ██╗████████╗███████╗███╗ ██╗████████╗███████╗ + ██║████╗ ██║╚══██╔══╝██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║██╔██╗ ██║ ██║ █████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║██║╚██╗██║ ██║ ██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ██║██║ ╚████║ ██║ ███████╗██║ ╚████║ ██║ ███████║ + ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ + + */ + + public createPaymentIntent = async (input: { + amount?: number; + currency?: CurrencyEnum; + paymentMethod?: string; + account?: AccountModel; + }): Promise => { + const { amount, currency, account, paymentMethod } = input; + + let customerId; + if (account) { + // Check if customer exists + if (account.paymentInfo && account.paymentInfo.stripeInfo) { + customerId = account.paymentInfo.stripeInfo.customerId; + } else customerId = await this.createCustomer(account); + } + + try { + const params: Stripe.PaymentIntentCreateParams = { + amount: Math.ceil((amount || 1) * 100), + currency: currency || CurrencyEnum.usd, + }; + if (customerId) params.customer = customerId; + if (paymentMethod) params.payment_method = paymentMethod; + + const paymentIntent = await this.stripe.paymentIntents.create(params); + + return paymentIntent; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + public updatePaymentIntent = async ( + paymentIntentId: string, + input: { + amount?: number; + currency?: CurrencyEnum; + paymentMethod?: string; + account?: AccountModel; + }, + ): Promise => { + const { amount, currency, account, paymentMethod } = input; + + let customerId; + if (account) { + // Check if customer exists + if (account.paymentInfo && account.paymentInfo.stripeInfo) { + customerId = account.paymentInfo.stripeInfo.customerId; + } else customerId = await this.createCustomer(account); + } + + try { + const params: Stripe.PaymentIntentUpdateParams | undefined = {}; + // TODO !! + if (amount) params.amount = Math.round(amount * 100); + if (currency) params.currency = currency; + if (customerId) params.customer = customerId; + if (paymentMethod) params.payment_method = paymentMethod; + + if (!_.isEmpty(params)) { + const paymentIntent = await this.stripe.paymentIntents.update(paymentIntentId, params); + return paymentIntent; + } + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + public confirmPaymentIntent = async (paymentIntentId: string, input: { paymentMethod: string }): Promise => { + const { paymentMethod } = input; + + try { + const paymentIntent = await this.stripe.paymentIntents.confirm(paymentIntentId, { payment_method: paymentMethod }); + return paymentIntent; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + /* + ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗████████╗ + ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██║ ██║██╔════╝╚══██╔══╝ + ██████╔╝██████╔╝██║ ██║██║ ██║██║ ██║██║ ██║ + ██╔═══╝ ██╔══██╗██║ ██║██║ ██║██║ ██║██║ ██║ + ██║ ██║ ██║╚██████╔╝██████╔╝╚██████╔╝╚██████╗ ██║ + ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ + + */ + + public createProduct = async (productInfo: { + _id: string; + name: string; + prices: { + amount: number; + currency: 'usd' | 'eur'; + recurring?: { + type: 'licensed' | 'metered'; + interval: 'week' | 'month' | 'year'; + intervalPeriod: number; + }; + }[]; + description?: string; + images?: string[]; + }): Promise<{ sProduct: Stripe.Product; sPrices: Stripe.Price[] }> => { + try { + const result: any = { sProduct: {}, sPrices: [] }; + // Create the product + const productCreate = await this.stripe.products.create({ + name: productInfo.name, + description: productInfo.description || productInfo.name, + active: true, + images: productInfo.images || [], + metadata: { _id: productInfo._id }, + statement_descriptor: productInfo.name, + }); + + result.sProduct = productCreate; + // Link all the prices + const productStripeId = productCreate.id; + + for (let index = 0; index < productInfo.prices.length; index++) { + const price = productInfo.prices[index]; + const priceInfo: Stripe.PriceCreateParams = { + currency: price.currency, + unit_amount: price.amount, + product: productStripeId, + metadata: { productId: productInfo._id }, + }; + + if (price.recurring) { + priceInfo.recurring = { + interval: price.recurring.interval, + interval_count: price.recurring.intervalPeriod, + usage_type: price.recurring.type, + }; + } + + result.sPrices.push(await this.stripe.prices.create(priceInfo)); + } + + return result; + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; + + // public updateProduct = async (paymentMethodId: string): Promise => { + // try { + // // Check if that card exists already + + // const paymentMethod = await this.stripe.paymentMethods.detach(paymentMethodId); + + // return paymentMethod; + // } catch (error) { + // throw newError(6000, { message: error.message }); + // } + // }; + + // public deleteProduct = async (paymentMethodId: string): Promise => { + // try { + // // Check if that card exists already + + // const paymentMethod = await this.stripe.paymentMethods.detach(paymentMethodId); + + // return paymentMethod; + // } catch (error) { + // throw newError(6000, { message: error.message }); + // } + // }; + + /* +████████╗██████╗ █████╗ ███╗ ██╗███████╗███████╗███████╗██████╗ ███████╗ +╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝ + ██║ ██████╔╝███████║██╔██╗ ██║███████╗█████╗ █████╗ ██████╔╝███████╗ + ██║ ██╔══██╗██╔══██║██║╚██╗██║╚════██║██╔══╝ ██╔══╝ ██╔══██╗╚════██║ + ██║ ██║ ██║██║ ██║██║ ╚████║███████║██║ ███████╗██║ ██║███████║ + ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ + + */ + + public reimburse = async (transactionId: string): Promise => { + try { + const stripeRes = await this.stripe.refunds.create({ payment_intent: transactionId }); + if (stripeRes.status == 'succeeded' || stripeRes.status == 'pending') { + return stripeRes; + } else { + throw newError(6000, { message: stripeRes.failure_reason }); + } + } catch (error) { + throw newError(6000, { message: error.message }); + } + }; +} diff --git a/services/module-payments/services/stripe/paymentIntent.helper.ts b/services/module-payments/services/stripe/paymentIntent.helper.ts new file mode 100644 index 0000000..e7c71e1 --- /dev/null +++ b/services/module-payments/services/stripe/paymentIntent.helper.ts @@ -0,0 +1,14 @@ +import { StripePayoutInfo } from '@services/module-payments/components/components.payments'; +import AccountModel from '@src/accounts/account.model'; +import Stripe from 'stripe'; + +export const updateStripePayoutInfo = async (account: AccountModel, stripeCreateAccount: Stripe.Response): Promise => { + const stripePayoutInfo: StripePayoutInfo = { + accountId: stripeCreateAccount.id, + payoutsEnabled: stripeCreateAccount.payouts_enabled, + chargesEnabled: stripeCreateAccount.charges_enabled, + detailsSubmitted: stripeCreateAccount.details_submitted, + }; + + return await account.updateOneCustom({ _id: account._id }, { $set: { 'payoutInfo.stripeInfo': stripePayoutInfo } }, null); +}; diff --git a/services/module-payments/services/stripe/payoutAccounts.helper.ts b/services/module-payments/services/stripe/payoutAccounts.helper.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/__components/components.ts b/src/__components/components.ts new file mode 100644 index 0000000..b7bf308 --- /dev/null +++ b/src/__components/components.ts @@ -0,0 +1,157 @@ +import { registerEnumType, ID } from 'type-graphql'; +import { ObjectType, InputType, Int, Field } from 'type-graphql'; +import { GraphQLJSONObject } from 'graphql-type-json'; +import ImageComponent from '@seed/interfaces/components'; +import { AddressComponent } from '@seed/interfaces/components.geo'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +export enum RessourceEnum { + 'workspaces' = 'workspaces', +} +registerEnumType(RessourceEnum, { + name: 'RessourceEnum', +}); + +export enum ListEnum { + 'equipment' = 'equipment', + 'feature' = 'feature', +} +registerEnumType(ListEnum, { + name: 'ListEnum', +}); + +export enum AvailableTranslation { + en = 'en', + fr = 'fr', + nl = 'nl', + de = 'de', +} +registerEnumType(AvailableTranslation, { + name: 'AvailableTranslation', +}); + +export enum WorkspaceTypeEnum { + meeting = 'meeting', + individual = 'individual', + open = 'open', +} +registerEnumType(WorkspaceTypeEnum, { + name: 'WorkspaceTypeEnum', +}); + +export enum ProductTypeEnum { + 'none' = 'none', + 'subscription' = 'subscription', +} +registerEnumType(ProductTypeEnum, { + name: 'ProductTypeEnum', +}); + +export enum CommentModelEnum { + bookings = 'bookings', +} +registerEnumType(CommentModelEnum, { + name: 'CommentModelEnum', +}); + +export enum BookingsRessourceEnum { + workspaces = 'workspaces', +} +registerEnumType(BookingsRessourceEnum, { + name: 'BookingsRessourceEnum', +}); + +export enum AvailableCurrency { + eur = 'eur', + usd = 'usd', +} +registerEnumType(AvailableCurrency, { + name: 'AvailableCurrency', +}); + +export enum ProductRessourceEnum { + 'workspaces' = 'workspaces', +} +registerEnumType(ProductRessourceEnum, { + name: 'ProductRessourceEnum', +}); + +/* + ██████╗ ██████╗ ███╗ ███╗██████╗ ██████╗ ███╗ ██╗███████╗███╗ ██╗████████╗███████╗ + ██╔════╝██╔═══██╗████╗ ████║██╔══██╗██╔═══██╗████╗ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝ + ██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║██╔██╗ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗ + ██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║██║╚██╗██║██╔══╝ ██║╚██╗██║ ██║ ╚════██║ + ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝██║ ╚████║███████╗██║ ╚████║ ██║ ███████║ + ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ +*/ + +@ObjectType() +@InputType('TranslatableInput') +export class TranslatableComponent { + @Field({ nullable: true }) + en?: string; + @Field() + fr: string; + @Field({ nullable: true }) + nl?: string; + @Field({ nullable: true }) + de?: string; +} + +@ObjectType() +@InputType('BuyerInfo') +export class BuyerInfoComponent { + @Field() + firstName: string; + + @Field() + lastName: string; + + @Field() + email: string; + + @Field({ nullable: true }) + company?: string; + + @Field({ nullable: true }) + vatNumber?: string; + + @Field(() => AddressComponent) + address: AddressComponent; +} + +@ObjectType() +@InputType('MainImage') +export class MainImageComponent { + @Field(() => ImageComponent, { nullable: true }) + main?: ImageComponent; + + @Field(() => ImageComponent, { nullable: true }) + one?: ImageComponent; + + @Field(() => ImageComponent, { nullable: true }) + two?: ImageComponent; + + @Field(() => ImageComponent, { nullable: true }) + three?: ImageComponent; + + @Field(() => ImageComponent, { nullable: true }) + four?: ImageComponent; +} + +@ObjectType() +export class WorkspaceOwnerComponent { + @Field(() => String, { nullable: true }) + firstName?: string; + + @Field(() => String, { nullable: true }) + lastName?: string; +} diff --git a/src/__indexes/__bootstrap.ts b/src/__indexes/__bootstrap.ts new file mode 100644 index 0000000..8e67f64 --- /dev/null +++ b/src/__indexes/__bootstrap.ts @@ -0,0 +1,29 @@ + +import { createApolloContext } from '@seed/graphql/Middleware'; +import { clog } from '@seed/helpers/Utils'; +import { BookingCronService } from '@services/module-booking/functions/crons/booking.cron.service'; +import OrderEngineResolver from '@services/module-payments/functions/orders/resolvers/app/app.resolver'; +import OrderModel from '@src/orders/order.model'; +import { afterOrderCancel, afterPaymentSuccess } from '@src/orders/services/order.service'; + +export const runBeforeAll = async () => { + + // const bservice = new BookingCronService(); + // bservice.removeDrafts(); + + try { + // const orderId = "655ef797-cb09-40e3-927a-8c1d9d668315"; + // afterPaymentSuccess(await new OrderModel().getOne({query:{_id:orderId}})) + + // const oResolver = new OrderEngineResolver(); + // const ctx = await createApolloContext(); + // oResolver.validateOrder(orderId) + return; + + } catch (error) { + + } + + + +}; diff --git a/src/__indexes/__collections.ts b/src/__indexes/__collections.ts new file mode 100644 index 0000000..f546f90 --- /dev/null +++ b/src/__indexes/__collections.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from 'type-graphql'; + +export enum ModelCollectionEnum { + 'streams' = 'stream.changes', + accounts = 'accounts', + workspaces = 'workspaces', + bookings = 'bookings', +} +registerEnumType(ModelCollectionEnum, { + name: 'ModelLoadersEnum', +}); diff --git a/src/__indexes/__loaders.ts b/src/__indexes/__loaders.ts new file mode 100644 index 0000000..85ea416 --- /dev/null +++ b/src/__indexes/__loaders.ts @@ -0,0 +1,68 @@ +import { buildLoader, DataLoader } from '@seed/services/database/LoaderService'; + +import AccountModel from '@src/accounts/account.model'; +import ListModel from '@services/module-cms/functions/lists/list.model'; +import CategoryModel from '@services/module-cms/functions/categories/category.model'; +import CommentModel from '@services/module-comments/functions/comments/comment.model'; +import WorkspaceModel from '@src/workspace/workspace.model'; +import BookingModel from '@services/module-booking/functions/bookings/bookings.engine.model'; +import StreamEngineModel from '@seed/engine/utils/streams/stream.model'; +import PromoModel from '@services/module-payments/functions/promos/promo.model'; +import NotificationModel from '@seed/services/notifications/notifications.model'; +import PageModel from '@services/module-cms/functions/pages/pages.model'; +// import MessageModel from '@services/api-messaging/functions/messages/message.model'; + +export default class Loaders { + /* MODULES */ + + listLoader: DataLoader; + categoryLoader: DataLoader; + + commentLoader: DataLoader; + + promoLoader: DataLoader; + + + /* APP */ + + accountLoader: DataLoader; + workspaceLoader: DataLoader; + + public constructor() { + /* MODULES */ + + this.listLoader = buildLoader(new ListModel()); + this.categoryLoader = buildLoader(new CategoryModel()); + + // this.commentLoader = buildLoader(new CommentModel()); + + this.promoLoader = buildLoader(new PromoModel()); + + + /* APP */ + + this.accountLoader = buildLoader(new AccountModel()); + + this.workspaceLoader = buildLoader(new WorkspaceModel()); + } + + // /* MODULES */ + // public commentRessourceLoader = (args?: GetArgs): DataLoader => { + // return buildOneToManyFilterLoader(new CommentModel(), 'ressourceId', { parentId: { $exists: false } }); + // }; + + // public commentRepliesLoader = (args?: GetArgs): DataLoader => { + // return buildOneToManyLoader(new CommentModel(), 'parentId'); + // }; +} + +export const modelsLoaders = { + 'stream.changes': new StreamEngineModel(), + streams: new StreamEngineModel(), + notifications: new NotificationModel(), + accounts: new AccountModel(), + pages: new PageModel(), + + workspaces: new WorkspaceModel(), + bookings: new BookingModel(), +}; diff --git a/src/__indexes/__resolvers.index.admin.ts b/src/__indexes/__resolvers.index.admin.ts new file mode 100644 index 0000000..5d9cff5 --- /dev/null +++ b/src/__indexes/__resolvers.index.admin.ts @@ -0,0 +1,25 @@ +import 'reflect-metadata'; +import { NonEmptyArray } from 'type-graphql/dist/interfaces/NonEmptyArray'; + +/* + * SEED & MODULES + */ +import AccountBaseResolver from '@services/module-accounts/account.resolver'; +import AccountAdminBaseResolver from '@services/module-accounts/account.resolver.admin'; + +/* + * APP RELATED + */ +import AccountResolver from '@src/accounts/account.resolver'; +import { ApiCMSAdminResolvers } from '@services/module-cms'; + +export const AdminResolvers = [ + /* Modules */ + ...ApiCMSAdminResolvers, + + AccountAdminBaseResolver, + AccountBaseResolver, + + /* App specific */ + AccountResolver, +]; diff --git a/src/__indexes/__resolvers.index.ts b/src/__indexes/__resolvers.index.ts new file mode 100644 index 0000000..de7c49f --- /dev/null +++ b/src/__indexes/__resolvers.index.ts @@ -0,0 +1,50 @@ +import 'reflect-metadata'; + +/* + * SEED & MODULES + */ +import AccountBaseResolver from '@services/module-accounts/account.resolver'; + +/* + * APP RELATED + */ +import AccountResolver from '@src/accounts/account.resolver'; +import GooglePlaceResolver from '@seed/services/geolocation/functions/GooglePlaceResolver'; +import { ApiCMSAppResolvers } from '@services/module-cms'; +import { PaymentAppEngineResolvers, PayoutAppEngineResolvers } from '@services/module-payments'; +import { AccountFavResolver } from '@services/module-favorites/functions/favLikes/favLikes.resolver'; + +import WorkspaceResolver from '@src/workspace/workspace.resolver'; +import { BookingEngineResolvers } from '@services/module-booking/functions/bookings/resolvers/bookings.engine.resolver'; +import CheckoutResolver, { OrderFieldResolver } from '@src/orders/order.resolver'; +import OrderEngineResolver from '@services/module-payments/functions/orders/resolvers/app/app.resolver'; +import { BookingResolvers } from '@src/bookings/resolvers/bookings.resolver'; +import WorkspaceFieldResolver from '@src/workspace/workspace.resolver.fields'; +import { ApiMessagingResolver } from '@services/api-messaging/handler'; + +export const AppResolvers = [ + /* Modules */ + ...ApiCMSAppResolvers, + ...ApiMessagingResolver, + + AccountBaseResolver, + GooglePlaceResolver, + + /* App specific */ + AccountResolver, + AccountFavResolver, + + WorkspaceResolver, + WorkspaceFieldResolver, + + ...BookingEngineResolvers, + ...BookingResolvers, + + OrderEngineResolver, + OrderFieldResolver, + + ...PaymentAppEngineResolvers, + ...PayoutAppEngineResolvers, + + CheckoutResolver, +]; diff --git a/src/accounts/account.components.ts b/src/accounts/account.components.ts new file mode 100644 index 0000000..9592e03 --- /dev/null +++ b/src/accounts/account.components.ts @@ -0,0 +1,152 @@ +import { AddressComponent } from '@seed/interfaces/components.geo'; +import { Field, InputType, ObjectType, registerEnumType } from 'type-graphql'; + +/* + ███████╗███╗ ██╗██╗ ██╗███╗ ███╗ + ██╔════╝████╗ ██║██║ ██║████╗ ████║ + █████╗ ██╔██╗ ██║██║ ██║██╔████╔██║ + ██╔══╝ ██║╚██╗██║██║ ██║██║╚██╔╝██║ + ███████╗██║ ╚████║╚██████╔╝██║ ╚═╝ ██║ + ╚══════╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ +*/ + +export enum AccountTypeEnum { + admin = 'admin', + user = 'user', + pro = 'pro', + public = 'public', + organisationOwner = 'organisationOwner', + storeManager = 'storeManager', +} +registerEnumType(AccountTypeEnum, { + name: 'AccountTypeEnum', +}); + +export enum PersonaEnum { + independant = 'independant', + ceo = 'ceo', + employee = 'employee', + association = 'association', + student = 'student', +} +registerEnumType(PersonaEnum, { + name: 'PersonaEnum', +}); + +export enum SocietyType { + independant = 'independant', + company = 'company', +} +registerEnumType(SocietyType, { + name: 'SocietyType', +}); + +export enum TransferType { + bankAccount = 'bankAccount', +} +registerEnumType(TransferType, { + name: 'TransferType', +}); + +export enum SettingsTypeEnum { + notifications = 'notifications', + promotions = 'promotions', + reminders = 'reminders', +} +registerEnumType(SettingsTypeEnum, { + name: 'SettingsTypeEnum', +}); + +@ObjectType() +@InputType('IndependantInfoComponentInput') +export class IndependantInfoComponent { + @Field() + companyNumber: string; +} + +@ObjectType() +@InputType('CeoInfoComponentInput') +export class CeoInfoComponent { + @Field() + companyNumber: string; + + @Field({ nullable: true }) + companyName?: string; + + @Field({ nullable: true }) + fiscalForm?: string; +} + +@ObjectType() +@InputType('EmployeeInfoComponentInput') +export class EmployeeInfoComponent { + @Field() + companyNumber: string; + + @Field({ nullable: true }) + companyName?: string; + + @Field({ nullable: true }) + fiscalForm?: string; + + @Field({ nullable: true }) + jobFunction?: string; +} + +@ObjectType() +@InputType('AssociationInfoComponentInput') +export class AssociationInfoComponent { + @Field() + companyNumber: string; + + @Field({ nullable: true }) + companyName?: string; + + @Field({ nullable: true }) + fiscalForm?: string; + + @Field({ nullable: true }) + jobFunction?: string; +} + +@ObjectType() +@InputType('StudentInfoComponentInput') +export class StudentInfoComponent { + @Field({ nullable: true }) + schoolName?: string; + + @Field({ nullable: true }) + sectionName?: string; + + @Field({ nullable: true }) + schoolYear?: string; +} + +@ObjectType() +@InputType('InvoiceProfileInput') +export class InvoiceProfile { + @Field(() => SocietyType) + type: SocietyType; + + @Field() + companyNumber: string; + + @Field({ nullable: true }) + companyName?: string; + + @Field({ nullable: true }) + fiscalForm?: string; + + @Field(() => AddressComponent, { nullable: true }) + address: AddressComponent; +} + +@ObjectType() +@InputType('TransferProfileInput') +export class TransferProfile { + @Field(() => TransferType) + type: TransferType; + + @Field() + accountNumber: string; +} diff --git a/src/accounts/account.helper.ts b/src/accounts/account.helper.ts new file mode 100644 index 0000000..879b0d1 --- /dev/null +++ b/src/accounts/account.helper.ts @@ -0,0 +1,3 @@ +import AccountModel from './account.model'; + +export const checkProfileCompletion = (model: AccountModel) => {}; diff --git a/src/accounts/account.model.ts b/src/accounts/account.model.ts new file mode 100644 index 0000000..b227198 --- /dev/null +++ b/src/accounts/account.model.ts @@ -0,0 +1,77 @@ +import { ObjectType, Field, ArgsType, InputType } from 'type-graphql'; +import { Equals } from 'class-validator'; + +import BaseAccountModel, { EditBaseAccountInput, NewBaseAccountInput } from '@services/module-accounts/account.model'; +import { Permission } from '@seed/interfaces/permission'; + +import { GetArgs } from '@seed/graphql/Request'; +import { AccountTypeEnum } from './account.components'; +import { FavoriteComponent } from '@services/module-favorites/components'; +import { AccountPaymentInfo, AccountPayoutInfo } from '@services/module-payments/components/components.payments'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.admin], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class AccountModel extends BaseAccountModel { + public constructor() { + super(permissions); + } + + @Field(() => AccountPaymentInfo, { nullable: true }) + paymentInfo?: AccountPaymentInfo; + + @Field(() => AccountPayoutInfo, { nullable: true }) + payoutInfo?: AccountPayoutInfo; + + @Field() + terms: boolean; + + @Field(() => [FavoriteComponent], { nullable: 'itemsAndList' }) + favLikes?: FavoriteComponent[]; + + // @Field(() => PersonaEnum, { nullable: true }) + // currentPersona?: PersonaEnum; + + // @Field(() => IndependantInfoComponent, { nullable: true }) + // independantInfo?: IndependantInfoComponent; + + // @Field(() => CeoInfoComponent, { nullable: true }) + // ceoInfo?: CeoInfoComponent; + + // @Field(() => EmployeeInfoComponent, { nullable: true }) + // employeeInfo?: EmployeeInfoComponent; + + // @Field(() => AssociationInfoComponent, { nullable: true }) + // associationInfo?: AssociationInfoComponent; + + // @Field(() => StudentInfoComponent, { nullable: true }) + // studentInfo?: StudentInfoComponent; + + searchOptions(): string[] { + return []; + } + async afterUpdate(): Promise { + // Check if current profile is complete + } +} + +@ArgsType() +export class AccountArgs { + @Field(() => GetArgs, { nullable: true }) + pagination?: GetArgs; +} + +@InputType() +export class NewAccountInput extends NewBaseAccountInput implements Partial { + @Field() + @Equals(true) + terms: boolean; +} + +@InputType() +export class EditAccountInput extends EditBaseAccountInput implements Partial {} diff --git a/src/accounts/account.resolver.ts b/src/accounts/account.resolver.ts new file mode 100644 index 0000000..e44ba77 --- /dev/null +++ b/src/accounts/account.resolver.ts @@ -0,0 +1,57 @@ +import { Resolver, Ctx, FieldResolver, Root, Mutation, Arg, Authorized } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; + +import AccountModel, { NewAccountInput, EditAccountInput } from '@src/accounts/account.model'; +import { signinGeneric } from '@services/module-accounts/services/AccountService'; +import { AccountTypeEnum } from './account.components'; +import { editOneGeneric } from '@seed/graphql/BaseService'; +import AccountBaseResolver from '@services/module-accounts/account.resolver'; +import { sendNotification } from '@seed/services/notifications/services/NotificationService'; +import { NotificationEnum } from '@config/config'; +import { AvailableTranslation } from '@src/__components/components'; + +@Resolver(AccountModel) +export default class AccountResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + + */ + + @Mutation(() => AccountModel) + async register(@Arg('input') input: NewAccountInput, @Ctx() ctx: ApolloContext): Promise { + const model = new AccountModel(); + const newAccount = await signinGeneric(model, AccountTypeEnum.user, input, ctx); + + await sendNotification({ + key: NotificationEnum.welcomeUser, + emails: [newAccount.email], + language: newAccount.language || AvailableTranslation.fr, + ressource: {...newAccount, siteLink: process.env.SITE_URL || 'https://work-in-flex.com'}, + }) + + return newAccount as any; + } +} diff --git a/src/accounts/account.service.ts b/src/accounts/account.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/bookings/resolvers/bookings.resolver.ts b/src/bookings/resolvers/bookings.resolver.ts new file mode 100644 index 0000000..86c32b7 --- /dev/null +++ b/src/bookings/resolvers/bookings.resolver.ts @@ -0,0 +1,140 @@ +import { EngineMiddleware } from '@seed/graphql/MiddlewareV2'; +import { StatusEnum } from '@seed/interfaces/components'; +import { ApolloContext } from '@seed/interfaces/context'; +import RoomModel from '@services/api-messaging/functions/rooms/room.model'; +import { roomsGetOneByAccountsIds } from '@services/api-messaging/functions/rooms/room.service'; +import { BookingService } from '@services/module-booking/functions/bookings/booking.service'; +import BookingModel from '@services/module-booking/functions/bookings/bookings.engine.model'; +import { BookingEngineMutationResolver } from '@services/module-booking/functions/bookings/resolvers/bookings.engine.resolver'; +import { BookingEngineNewInputSchema } from '@services/module-booking/functions/bookings/schemas/bookings.engine.input'; +import { BookingEngineSchema } from '@services/module-booking/functions/bookings/schemas/bookings.engine.schema'; +import { DurationTypeEnum } from '@services/module-booking/functions/components'; +import { formatSlot, formatStartToEnd, SlotFormat, StartToEndFormat } from '@services/module-booking/functions/helper'; +import { OrderEngineSchema } from '@services/module-payments/functions/orders/schemas/order.schema'; +import { beginCheckoutWithOrder, finaliseCheckoutWithOrder } from '@services/module-payments/functions/orders/services/OrderService'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import WorkspaceModel from '@src/workspace/workspace.model'; +import { BookingsRessourceEnum } from '@src/__components/components'; +import { ModelCollectionEnum } from '@src/__indexes/__collections'; +import _ from 'lodash'; +import { Arg, Authorized, Ctx, FieldResolver, Mutation, Resolver, Root } from 'type-graphql'; +import { BookingNewInputSchema } from '../schemas/bookings.input'; +import { BookingSchema } from '../schemas/bookings.schema'; + +@Resolver(BookingEngineSchema) +export class BookingFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + @FieldResolver(() => WorkspaceModel, { nullable: true }) + async workspace(@Root() booking: BookingSchema, @Ctx() ctx: ApolloContext): Promise { + // Get the correct booking model + const ref = _.find(booking.paths, { ressourceModel: BookingsRessourceEnum.workspaces }); + if (ref) return ctx.ctx.loaders.workspaceLoader.load(ref.ressourceId); + return; + } + + @FieldResolver(() => RoomModel, { nullable: true }) + async room(@Root() booking: BookingSchema, @Ctx() ctx: ApolloContext): Promise { + const workspace = await this.workspace(booking, ctx); + if (workspace && workspace.createdBy) { + const workspaceOwner = workspace.createdBy; + const bookingUser = booking.ownerId; + + return roomsGetOneByAccountsIds([workspaceOwner, bookingUser], { ctx }); + } + return; + } +} + +@Resolver(BookingSchema) +export class BookingQueryResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ +} + +@Resolver(BookingSchema) +export class BookingMutationResolver { + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ + + @Mutation(() => OrderEngineSchema) + @EngineMiddleware({ + authorization: [AccountTypeEnum.user], + validations: [{ schema: BookingEngineNewInputSchema }], + }) + async reserveOneBooking(@Arg('input') input: BookingNewInputSchema, @Ctx() ctx: ApolloContext): Promise { + const zone = process.env.TIMEZONE || 'utc'; + + const workspace = await new WorkspaceModel().getOne({ _id: input.ressourceId }, ctx); + + const removeWeekend = workspace.availability.noWeekend || false; + + let formatted: StartToEndFormat | SlotFormat; + + if (input.durationType == DurationTypeEnum.startToEnd) formatted = formatStartToEnd(input.startToEnd!, { zone }); + else formatted = formatSlot(input.slot!, { zone, removeWeekend }); + + // Create the booking + // Remove all the previous draft + await (await new BookingModel().db()).deleteMany({ ownerId: ctx.ctx.user._id, status: StatusEnum.draft }); + const data = await new BookingService().createOneBookingWithModel({ + model: workspace, + input, + status: StatusEnum.draft, + availabilities: workspace.availability, + ctx, + }); + + // Get the quantityHours + const quantityHours = formatted.bookingDurationInMinutes / 60; + // TODO : Add taxes + + return await beginCheckoutWithOrder({ + lines: [workspace.getLineProduct({ quantity: quantityHours })], + contactInfo: input.checkoutInfo, + input, + paths: [{'ressourceModel':ModelCollectionEnum.bookings,ressourceId:data._id},{'ressourceModel':ModelCollectionEnum.workspaces,ressourceId:workspace._id}], + ctx, + }); + + // Send the notifications + // const promises = [ + // sendNotification({ + // key: 'agendaUserConfirm', + // emails: [ctx.ctx.user.email], + // language: ctx.ctx.user.language, + // ressource: { formatted, pro, user, ...data }, + // }), + // sendNotification({ + // key: 'agendaProConfirm', + // emails: [pro.email || pro.googleEmail], + // language: ctx.ctx.user.language, + // ressource: { formatted, pro, user, ...data }, + // }), + // ]; + // await promiseAll(promises); + + // return data; + } +} + +export const BookingResolvers = [BookingFieldResolver, BookingQueryResolver, BookingMutationResolver]; diff --git a/src/bookings/resolvers/getEventDescription.ts b/src/bookings/resolvers/getEventDescription.ts new file mode 100644 index 0000000..1048866 --- /dev/null +++ b/src/bookings/resolvers/getEventDescription.ts @@ -0,0 +1,18 @@ +import { interpolate } from '@seed/helpers/Utils'; +import EmailSettingsModel from '@services/module-cms/functions/emails-settings/emails-settings.model'; +import { AvailableTranslation } from '@src/__components/components'; + +export const getEventDescription = async (input: any, language: AvailableTranslation = AvailableTranslation.fr): Promise => { + const emailSettings = new EmailSettingsModel(); + try { + await emailSettings.getOne({ key: 'agendaProConfirm' }, null); + } catch (error) { + console.error('NOTIFICATION.ERROR', { message: 'notfound', key: 'agendaProConfirm' }); + return ''; + } + + const body = emailSettings.body && emailSettings.body[language] ? emailSettings.body[language] : ''; + const string = interpolate(body || '', input); + + return string; +}; diff --git a/src/bookings/schemas/bookings.input.ts b/src/bookings/schemas/bookings.input.ts new file mode 100644 index 0000000..1bcd09e --- /dev/null +++ b/src/bookings/schemas/bookings.input.ts @@ -0,0 +1,20 @@ +import { Field, InputType, Int, ArgsType, ObjectType } from 'type-graphql'; + +import { BookingEngineArgsSchema, BookingEngineNewInputSchema } from '@services/module-booking/functions/bookings/schemas/bookings.engine.input'; +import { CheckoutEngineInput } from '@services/module-payments/functions/orders/schemas/order.schema.input'; + +@ObjectType('BookingNewInput') +@InputType() +export class BookingNewInputSchema extends BookingEngineNewInputSchema { + @Field(() => Int, { nullable: true }) + capacity: number; + + @Field({ nullable: true }) + comments?: string; + + @Field(() => CheckoutEngineInput, { nullable: true }) + checkoutInfo?: CheckoutEngineInput; +} + +@ArgsType() +export class BookingArgsSchema extends BookingEngineArgsSchema {} diff --git a/src/bookings/schemas/bookings.schema.ts b/src/bookings/schemas/bookings.schema.ts new file mode 100644 index 0000000..34bfbe0 --- /dev/null +++ b/src/bookings/schemas/bookings.schema.ts @@ -0,0 +1,22 @@ +import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema'; +import { BookingEngineDBInterfaceSchema } from '@services/module-booking/functions/bookings/schemas/bookings.engine.schema'; +import { Field, Int, ObjectType } from 'type-graphql'; + +@ObjectType() +export class BookingDBInterfaceSchema extends BookingEngineDBInterfaceSchema { + @Field(() => Int) + capacity: number; +} + +@ObjectType() +export class BookingDBSchema extends BookingDBInterfaceSchema {} + +@ObjectType({ implements: IEngineSchema }) +export class BookingSchema extends BookingDBSchema implements IEngineSchema { + _id: string; + organisationId?: string | undefined; + by?: MetaBy | undefined; + permissions: MetaPermissions; + createdAt: Date; + updatedAt: Date; +} diff --git a/src/orders/components/checkout.schema.ts b/src/orders/components/checkout.schema.ts new file mode 100644 index 0000000..27e3b10 --- /dev/null +++ b/src/orders/components/checkout.schema.ts @@ -0,0 +1,6 @@ +import { ObjectType, Field, Ctx, InputType } from 'type-graphql'; + +import { CheckoutEngineInput } from '@services/module-payments/functions/orders/schemas/order.schema.input'; + +@InputType() +export class CheckoutInput extends CheckoutEngineInput {} diff --git a/src/orders/components/components.cart.ts b/src/orders/components/components.cart.ts new file mode 100644 index 0000000..610e5d1 --- /dev/null +++ b/src/orders/components/components.cart.ts @@ -0,0 +1,10 @@ +import { OrderEngineNewSchema } from '@services/module-payments/functions/orders/schemas/order.schema.input'; +import { ObjectType, InputType } from 'type-graphql'; + +@ObjectType() +@InputType('OrderNewInput') +export class OrderNewInputSchema extends OrderEngineNewSchema {} + +@ObjectType() +@InputType('CartLineOptionInputComponent') +export class CartLineOptionComponent {} diff --git a/src/orders/components/line.schema.ts b/src/orders/components/line.schema.ts new file mode 100644 index 0000000..db0f3bc --- /dev/null +++ b/src/orders/components/line.schema.ts @@ -0,0 +1,44 @@ +import { ObjectType, Field, Ctx, InputType } from 'type-graphql'; + +import { ApolloContext, ApolloContextLoadersOnly } from '@seed/interfaces/context'; +import { LineItemGenericSchema } from '@services/module-payments/functions/orders/carts/schemas/line.schema'; +import { ProductRessourceEnum } from '@src/__components/components'; +import WorkspaceModel from '@src/workspace/workspace.model'; + +import { createUnionType } from 'type-graphql'; +import { plainToClass } from 'class-transformer'; +import { linePrice, lineVatPrice } from '../services/line.service'; + +export const ProductUnion = createUnionType({ + name: 'ProductUnion', // the name of the GraphQL union + types: () => [WorkspaceModel] as const, // function that returns tuple of object types classes +}); + +@InputType('LineItemSchemaInput') +@ObjectType() +export class LineItemBaseSchema extends LineItemGenericSchema { + @Field(() => ProductUnion, { nullable: true }) + async getProduct(@Ctx() ctx: ApolloContext): Promise { + if (this.productRessource == ProductRessourceEnum.workspaces) { + return plainToClass(WorkspaceModel, await ctx.ctx.loaders.workspaceLoader.load(this.productId)); + } + + return; + } + + @Field(() => Number) + async getLinePrice(@Ctx() ctx: ApolloContext): Promise { + return await linePrice(this, ctx); + } + + @Field(() => Number) + async getLineVatPrice(@Ctx() ctx: ApolloContext): Promise { + return await lineVatPrice(this, ctx); + } +} + +@ObjectType() +export class LineItemDBSchema extends LineItemBaseSchema {} + +@ObjectType() +export class LineItemSchema extends LineItemBaseSchema {} diff --git a/src/orders/order.model.ts b/src/orders/order.model.ts new file mode 100644 index 0000000..3d0e617 --- /dev/null +++ b/src/orders/order.model.ts @@ -0,0 +1,23 @@ +import { IEngineSchema } from '@seed/engine/EngineSchema'; +import OrderEngineModel from '@services/module-payments/functions/orders/order.model'; +import { OrderEngineDBInterfaceSchema, OrderEngineSchema } from '@services/module-payments/functions/orders/schemas/order.schema'; +import { ObjectType, Field, ArgsType } from 'type-graphql'; + +@ObjectType() +export default class OrderModel extends OrderEngineModel { + public constructor(input?: OrderEngineDBInterfaceSchema & Partial) { + super(input); + } + + searchOptions(): string[] { + return []; + } + + async afterPaymentSuccess(order:OrderEngineSchema): Promise { + console.log('ok', this._id); + } + + async afterPaymentFailed(order:OrderEngineSchema): Promise { + console.log('ok', this._id); + } +} diff --git a/src/orders/order.resolver.admin.ts b/src/orders/order.resolver.admin.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/orders/order.resolver.ts b/src/orders/order.resolver.ts new file mode 100644 index 0000000..4dee971 --- /dev/null +++ b/src/orders/order.resolver.ts @@ -0,0 +1,55 @@ +import { Resolver, Authorized, Ctx, FieldResolver, Root } from 'type-graphql'; +import { Mutation, Arg } from 'type-graphql'; +import { ApolloContext } from '@seed/interfaces/context'; + +// From Module + +// From SRC +import OrderModel from '@src/orders/order.model'; +import CheckoutEngineResolver from '@services/module-payments/functions/orders/resolvers/app/checkout.resolver'; +import { OrderEngineSchema } from '@services/module-payments/functions/orders/schemas/order.schema'; +import RoomModel from '@services/api-messaging/functions/rooms/room.model'; +import { roomsGetOneByAccountsIds } from '@services/api-messaging/functions/rooms/room.service'; +import { BookingSchema } from '@src/bookings/schemas/bookings.schema'; +import WorkspaceModel from '@src/workspace/workspace.model'; +import { BookingsRessourceEnum } from '@src/__components/components'; +import _ from 'lodash'; +import { BookingNewInputSchema } from '@src/bookings/schemas/bookings.input'; + +@Resolver(OrderEngineSchema) +export class OrderFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + @FieldResolver(() => BookingNewInputSchema, { nullable: true }) + async getBookingInput(@Root() order: OrderEngineSchema, @Ctx() ctx: ApolloContext): Promise { + if (order.inputData) return order.inputData; + return; + } +} + +@Resolver(OrderModel) +export default class CheckoutResolver extends CheckoutEngineResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/src/orders/services/line.service.ts b/src/orders/services/line.service.ts new file mode 100644 index 0000000..47b3594 --- /dev/null +++ b/src/orders/services/line.service.ts @@ -0,0 +1,56 @@ +import { ApolloContext } from '@seed/interfaces/context'; + +import VatModel from '@services/module-payments/functions/vat/vat.model'; +import PromoModel from '@services/module-payments/functions/promos/promo.model'; +import { LineItemDBSchema, ProductUnion } from '@src/orders/components/line.schema'; + +const getProductPrice = async (product: typeof ProductUnion | undefined, quantity: number, ctx: ApolloContext, promoId?: string): Promise => { + let price = product ? (await product.finalPrice()) * quantity : 0; + if (promoId) { + const promo = await new PromoModel().getOne({ _id: promoId }, ctx); + price -= (price * promo.value) / 100; + } + return price; +}; + +const getProductVatPrice = async ( + product: typeof ProductUnion | undefined, + quantity: number, + countryCode: string | undefined, + ctx: ApolloContext, + promoId?: string, +): Promise => { + let vatPrice = 0; + + let vatClass: VatModel; + + if ((product as any).vatClassId) + vatClass = await new VatModel().getOne({ _id: (product as any).vatClassId }, ctx); + else vatClass = await new VatModel().getOne({ 'title': 'standard' }, ctx); + + if (!countryCode) countryCode = process.env.PAYMENT_DEFAULT_COUNTRY || 'BE'; + + const vatValue = vatClass.values[countryCode]; + vatPrice += ((await getProductPrice(product, quantity, ctx)) * vatValue) / 100; + + + if (promoId) { + const promo = await new PromoModel().getOne({ _id: promoId }, ctx); + vatPrice -= (vatPrice * promo.value) / 100; + } + return vatPrice; +}; + +export const linePrice = async (line: LineItemDBSchema, ctx: ApolloContext): Promise => { + let price = 0; + price += await getProductPrice(await line.getProduct(ctx), line.quantity, ctx, line.promoId); + + return price; +}; + +export const lineVatPrice = async (line: LineItemDBSchema, ctx: ApolloContext): Promise => { + let vatPrice = 0; + vatPrice += await getProductVatPrice(await line.getProduct(ctx), line.quantity, line.localeInfo?.countryCode, ctx); + + return vatPrice; +}; diff --git a/src/orders/services/order.service.ts b/src/orders/services/order.service.ts new file mode 100644 index 0000000..612f1d1 --- /dev/null +++ b/src/orders/services/order.service.ts @@ -0,0 +1,186 @@ +import _ from "lodash"; +import { promiseAll } from "@seed/helpers/Utils"; + +import { OrderEngineSchema } from "@services/module-payments/functions/orders/schemas/order.schema"; +import { StatusEnum } from "@seed/interfaces/components"; + +import { ModelCollectionEnum } from "@src/__indexes/__collections"; +import AccountModel from "@src/accounts/account.model"; +import BookingModel from "@services/module-booking/functions/bookings/bookings.engine.model"; +import { sendNotification } from "@seed/services/notifications/services/NotificationService"; +import { NotificationEnum } from "@config/config"; + +import moment from "moment-timezone"; +import { BookingSchema } from "@src/bookings/schemas/bookings.schema"; +import WorkspaceModel from "@src/workspace/workspace.model"; + +//get booking & workspace path +export const getPaths = (order: OrderEngineSchema) => { + const paths = { + bookingPath: _.find(order.paths,{'ressourceModel':ModelCollectionEnum.bookings}), + workspacePath: _.find(order.paths,{'ressourceModel':ModelCollectionEnum.workspaces}), + } + + if (!paths.bookingPath || !paths.workspacePath) { + console.error('SYSTEM ERROR', 'NO BOOKING IN ORDERS') + return + } + + return paths; +} + +//get workspace of order +export const getWorkspace = async(workspacePath): Promise => { + return await new WorkspaceModel().getOne({_id: workspacePath?.ressourceId}, null); +} + +//get owner +export const getOwner = async(ownerId): Promise => { + return await new AccountModel().getOne({_id: ownerId}, null); +} + +//transform slots to string to pass to email +export const transformSlotsToString = (booking: BookingSchema) => { + let slots = ''; + booking.dates.map((item) => { + slots += `
  • ${moment.tz(item.startDate, "Europe/Brussels").format('DD/MM/YYYY')}, ${booking.slot?.startTime} - ${booking.slot?.endTime}
  • `; + }); + + return slots; +} + +//get booking +export const getBooking = async(bookingPath): Promise => { + return await new BookingModel().getOne({ + query:{'_id': bookingPath?.ressourceId}} + ); +} + +export const afterPaymentSuccess = async (order: OrderEngineSchema) => { + const promises: Promise[] = []; + + + const accountModel = new AccountModel(); + const client = await accountModel.getOne({ _id: order.accountId }, null); + + /* + * (1) Make the booking in validated + */ + const paths = getPaths(order); + + const booking = await getBooking(paths?.bookingPath); + + await new BookingModel().updateOne({ + query:{'_id': paths?.bookingPath?.ressourceId},newData: { + status:StatusEnum.validated + }} + ); + + + /* + * (2) Create Invoice + */ + + //line.getPrice doesnt work online for some reason + // await Promise.all( + // order.lines.map(async (line) => { + // return await StripeService.getInstance().createInvoiceItem( + // account.paymentInfo?.stripeInfo?.customerId || '', + // line.price, + // // (await line.getLinePrice(ctx)) + (await line.getLineVatPrice(ctx)), + // line.title, + // order.currency, + // ['txr_1IVZECCvwkniRa7bJi8ZiLhN'], + // ); + // }), + // ); + + // const invoice = await StripeService.getInstance().createInvoice(account.paymentInfo?.stripeInfo?.customerId || '', 'test'); + // await StripeService.getInstance().payInvoice(invoice.id); + + + /* + * (3) Send email + */ + //get workspace + const workspace = await getWorkspace(paths?.workspacePath); + + //get owner + const owner = await getOwner(workspace.createdBy); + + //format dates & slots to string to pass to email + const slots = transformSlotsToString(booking); + + //shortId to display in email + const shortId = booking._id.substr(booking._id.length - 5, 5); + + //send confirm of booking to owner + promises.push( + sendNotification({ + key: NotificationEnum.bookingProConfirm, + emails: [owner.email], + language: owner.language, + ressource: {contact: client.email, shortId, bookingLink: `${process.env.BOOKING_URL}${workspace._id}`, workspace, slots} + }) + ) + //send confirm of booking to client + promises.push( + sendNotification({ + key: NotificationEnum.bookingUserConfirm, + emails: [client.email], + language: client.language, + ressource: {contact: owner.email, shortId, bookingLink: `${process.env.BOOKING_URL}${workspace._id}`, workspace, slots } + }) + ) + + + await promiseAll(promises); +} + +export const afterOrderCancel = async (order: OrderEngineSchema) => { + const promises: Promise[] = []; + + const client = await new AccountModel().getOne({ _id: order.accountId }, null); + + const paths = getPaths(order); + + //get booking + const booking = await getBooking(paths?.bookingPath); + + //get workspace + const workspace = await getWorkspace(paths?.workspacePath); + + //get owner + const owner = await getOwner(workspace.createdBy); + + //format dates & slots to string to pass to email + const slots = transformSlotsToString(booking); + + //shortId to display in email + const shortId = booking._id.substr(booking._id.length - 5, 5); + + //send confirm cancellation of booking to client + promises.push( + sendNotification({ + key: NotificationEnum.bookingUserCancel, + emails: [client.email], + language: client.language, + ressource: {shortId, link: `${process.env.SITE_URL}`, workspace, slots } + }) + ) + + //send confirm cancellation of booking to owner + promises.push( + sendNotification({ + key: NotificationEnum.bookingProCancel, + emails: [owner.email], + language: owner.language, + ressource: {shortId, workspace, slots} + }) +) + + await promiseAll(promises); +} + +export const afterPaymentFailed = async (order: OrderEngineSchema) => { +} \ No newline at end of file diff --git a/src/workspace/workspace.model.ts b/src/workspace/workspace.model.ts new file mode 100644 index 0000000..cb41ae5 --- /dev/null +++ b/src/workspace/workspace.model.ts @@ -0,0 +1,288 @@ +import { ObjectType, Field, ArgsType, InputType, Int } from 'type-graphql'; + +import { Permission } from '@seed/interfaces/permission'; +// import CommentModel from '@services/module-comments/functions/comments/comment.model'; +import { AvailableCurrency, MainImageComponent, ProductRessourceEnum, WorkspaceTypeEnum } from '@src/__components/components'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import { CustomSearchEngine } from '@seed/services/database/DBRequestService'; +import BaseGeoSimpleSEOModel, { + NewGeoBaseSEOSimpleInput, + EditGeoBaseSEOSimpleInput, + BaseGeoArgs, + NewGeoWithAddressBaseSEOSimpleInput, + EditGeoWithAddressBaseSEOSimpleInput, +} from '@seed/services/geolocation/models/BaseGeoSimpleModel'; +import { RangeType } from '@seed/interfaces/components'; +import { AvailabilityComponent } from '@seed/interfaces/components.dates'; +import { + LineProductBaseInterface, + LineProductFunctionInterface, + LineProductInterface, +} from '@services/module-payments/functions/orders/carts/schemas/line.schema'; + +import { v4 as uuid } from 'uuid'; +import { EditBaseSEOSimpleInput, NewBaseSEOSimpleInput } from '@seed/graphql/baseModels/BaseSeoSimpleModel'; +import { AddressComponent, AddressStrictComponent } from '@seed/interfaces/components.geo'; +import { getGeoLocFromAddress, getPlaceFromAddress } from '@seed/services/geolocation/services/GooglePlaceService'; + +const permissions: Permission = { + c: [AccountTypeEnum.admin], + r: [AccountTypeEnum.public], + w: [AccountTypeEnum.admin], + d: [AccountTypeEnum.admin], +}; + +@ObjectType() +export default class WorkspaceModel extends BaseGeoSimpleSEOModel implements LineProductBaseInterface { + public constructor(collectionName?: string, perm?: Permission) { + const cName = collectionName ? collectionName : 'workspaces'; + const permission = perm ? perm : permissions; + super(cName, permission); + } + + @Field(() => WorkspaceTypeEnum) + workspaceType: WorkspaceTypeEnum; + + @Field(() => Int) + pricingPerHour: number; + + @Field(() => AvailableCurrency) + currency: AvailableCurrency; + + @Field(() => Int) + maxCapacity: number; + + @Field(() => Int) + minStay: number; + @Field(() => Int, { nullable: true }) + maxStay?: number; + + @Field(() => AvailabilityComponent) + availability: AvailabilityComponent; + + @Field(() => MainImageComponent) + mainImages: MainImageComponent; + + @Field(() => [String]) + equipmentIds: string[]; + @Field(() => [String]) + featureIds: string[]; + + + @Field(() => AddressComponent) + address(): AddressComponent { + return this.place.address; + } + + @Field() + finalPrice(): number { + // if (this.currentPromo) { + // if (this.currentPromo.promoType == PromoType.percentage) return this.pricingPerHour * (1 - this.currentPromo.value / 100); + // else return this.pricingPerHour - this.currentPromo.value; + // } + return this.pricingPerHour; + } + + /* + * PROMOS + */ + // @Field(() => CurrentPromo, { nullable: true }) + // currentPromo?: CurrentPromo; + + /* + * FIELDS RESOLVERS + */ + + // @Field(() => [CommentModel], { nullable: true }) + // comments?: CommentModel[]; + + searchOptions(): string[] { + return ['workspaceType']; + } + + filterOptions(): string[] { + return ['workspaceType']; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + async onCreate(input:NewWorkspaceInput): Promise { + input.place = await getPlaceFromAddress(input.address) + } + + async onUpdate(input:EditWorkspaceInput): Promise { + if (input.address) input.place = await getPlaceFromAddress(input.address) + } + + getLineProduct(input: { quantity: number }): LineProductInterface { + return { + _id: uuid(), + productRessource: ProductRessourceEnum.workspaces, + productId: this._id, + + title: this.title || 'Workspace - ' + this._id, + + quantity: input.quantity, + price: this.pricingPerHour, + salesPrice: this.pricingPerHour, + + finalPrice: this.finalPrice, + }; + } + + getLineProductFunctions(): LineProductFunctionInterface { + return {}; + } + + searchEngine(): CustomSearchEngine { + return { + workspaceType: { operation: '$eq' }, + workspaceTypes: { dbFName: 'workspaceType', operation: '$in' }, + + equipmentIds: { operation: '$in' }, + featureIds: { operation: '$in' }, + + pricingPerHour: { + operation: 'between', + beetweenConfig: { + inputFromField: 'priceMin', + inputToField: 'priceMax', + }, + }, + + from : { + operation:'$gte', + dbFName:'availability.dates.endDate' + }, + + date: { + operation: 'dateRange', + dateRangeConfig: { + dbFromField: 'availability.dates.startDate', + dbToField: 'availability.dates.endDate', + inputFromField: 'dateFrom', + inputToField: 'dateTo', + dateRangeType: RangeType.included, + }, + }, + + hour: { + operation: 'hourRange', + hourRangeConfig: { + dbMainField: 'availability.hours', + dbSubfieldFrom: 'from', + dbSubfieldTo: 'to', + inputFromField: 'hoursFrom', + inputToField: 'hoursTo', + dateRangeType: RangeType.included, + }, + }, + + maxCapacity: { + operation: '$gte', + }, + + createdBy: { + operation: '$eq', + }, + }; + } +} + +@ArgsType() +export class WorkspaceArgs extends BaseGeoArgs { + @Field({ nullable: true }) + dateFrom?: Date; + @Field({ nullable: true }) + dateTo?: Date; + + @Field({ nullable: true }) + hoursFrom?: string; + @Field({ nullable: true }) + hoursTo?: string; + + @Field(() => WorkspaceTypeEnum, { nullable: true }) + workspaceType?: WorkspaceTypeEnum; + @Field(() => [WorkspaceTypeEnum], { nullable: true }) + workspaceTypes?: WorkspaceTypeEnum[]; + + @Field(() => Int, { nullable: true }) + priceMin?: number; + @Field(() => Int, { nullable: true }) + priceMax?: number; + @Field(() => Int, { nullable: true }) + maxCapacity?: number; + + @Field(() => [String], { nullable: true }) + equipmentIds?: string[]; + @Field(() => [String], { nullable: true }) + featureIds?: string[]; +} + +@InputType() +export class NewWorkspaceInput extends NewBaseSEOSimpleInput { + @Field(() => WorkspaceTypeEnum) + workspaceType: WorkspaceTypeEnum; + + @Field(() => Int) + pricingPerHour: number; + @Field(() => AvailableCurrency) + currency: AvailableCurrency; + + @Field(() => Int) + maxCapacity: number; + + @Field(() => Int) + minStay: number; + @Field(() => Int, { nullable: true }) + maxStay?: number; + + @Field(() => AvailabilityComponent) + availability: AvailabilityComponent; + + @Field(() => MainImageComponent) + mainImages: MainImageComponent; + + @Field(() => [String]) + equipmentIds: string[]; + @Field(() => [String]) + featureIds: string[]; + + @Field(() => AddressStrictComponent) + address:AddressStrictComponent + + place: { loc: any; formattedAddress: string; }; +} + +@InputType() +export class EditWorkspaceInput extends EditBaseSEOSimpleInput { + @Field(() => WorkspaceTypeEnum, { nullable: true }) + workspaceType?: WorkspaceTypeEnum; + + @Field(() => Int, { nullable: true }) + pricingPerHour?: number; + @Field(() => AvailableCurrency) + currency: AvailableCurrency; + + @Field(() => Int, { nullable: true }) + maxCapacity?: number; + + @Field(() => Int, { nullable: true }) + minStay?: number; + @Field(() => Int, { nullable: true }) + maxStay?: number; + + @Field(() => AvailabilityComponent, { nullable: true }) + availability?: AvailabilityComponent; + + @Field(() => MainImageComponent) + mainImages: MainImageComponent; + + @Field(() => [String], { nullable: true }) + equipmentIds?: string[]; + @Field(() => [String], { nullable: true }) + featureIds?: string[]; + + @Field(() => AddressStrictComponent, {nullable:true}) + address:AddressStrictComponent + place: { loc: any; formattedAddress: string; }; +} diff --git a/src/workspace/workspace.resolver.admin.ts b/src/workspace/workspace.resolver.admin.ts new file mode 100644 index 0000000..f4c3d07 --- /dev/null +++ b/src/workspace/workspace.resolver.admin.ts @@ -0,0 +1,29 @@ +// import { Resolver, Query, FieldResolver, Root, Arg, Mutation, Ctx, Args, Authorized, ResolverInterface } from 'type-graphql'; + +// import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +// import { AccountTypeEnum } from '@src/accounts/account.components'; +// import ProductModel from './workspace.model'; + +// const ProductBaseAdminResolver = createBaseResolver('products', ProductModel, ProductArgs, NewProductInput, EditProductInput, [ +// AccountTypeEnum.admin, +// ]); + +// @Resolver(ProductModel) +// export default class ProductAdminResolver extends ProductBaseAdminResolver { +// /* +// ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ +// ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ +// ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ +// ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ +// ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ +// ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ +// */ +// /* +// ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ +// ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ +// ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ +// ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ +// ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ +// ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ +// */ +// } diff --git a/src/workspace/workspace.resolver.fields.ts b/src/workspace/workspace.resolver.fields.ts new file mode 100644 index 0000000..0e883f3 --- /dev/null +++ b/src/workspace/workspace.resolver.fields.ts @@ -0,0 +1,62 @@ +import { Resolver, FieldResolver, Root, Ctx, Args } from 'type-graphql'; + +import { ApolloContext } from '@seed/interfaces/context'; +import { oneToManyComplexity } from '@seed/graphql/Middleware'; +import ProductModel from './workspace.model'; +import ListModel from '@services/module-cms/functions/lists/list.model'; +import { BookingEngineSchema } from '@services/module-booking/functions/bookings/schemas/bookings.engine.schema'; +import WorkspaceModel from './workspace.model'; +import { BookingService } from '@services/module-booking/functions/bookings/booking.service'; +import { BookingArgsSchema } from '@src/bookings/schemas/bookings.input'; +import { BookingSchema } from '@src/bookings/schemas/bookings.schema'; +import { WorkspaceOwnerComponent } from '@src/__components/components'; + +@Resolver(WorkspaceModel) +export default class WorkspaceFieldResolver { + /* + ███████╗██╗███████╗██╗ ██████╗ ███████╗ + ██╔════╝██║██╔════╝██║ ██╔══██╗██╔════╝ + █████╗ ██║█████╗ ██║ ██║ ██║███████╗ + ██╔══╝ ██║██╔══╝ ██║ ██║ ██║╚════██║ + ██║ ██║███████╗███████╗██████╔╝███████║ + ╚═╝ ╚═╝╚══════╝╚══════╝╚═════╝ ╚══════╝ + */ + + @FieldResolver(() => [ListModel], { complexity: oneToManyComplexity, nullable: 'itemsAndList' }) + async getEquipments(@Root() product: ProductModel, @Ctx() ctx: ApolloContext): Promise<(ListModel | Error)[]> { + if (product.equipmentIds) return await ctx.ctx.loaders.listLoader.loadMany(product.equipmentIds); + return []; + } + + @FieldResolver(() => [ListModel], { complexity: oneToManyComplexity, nullable: 'itemsAndList' }) + async getFeatures(@Root() product: ProductModel, @Ctx() ctx: ApolloContext): Promise<(ListModel | Error)[]> { + if (product.featureIds) return await ctx.ctx.loaders.listLoader.loadMany(product.featureIds); + return []; + } + + @FieldResolver(() => [BookingSchema], { complexity: oneToManyComplexity, nullable: 'itemsAndList' }) + async getBookings(@Root() workspace: WorkspaceModel, @Args() args: BookingArgsSchema, @Ctx() ctx: ApolloContext): Promise { + return new BookingService().getBookings(args, ctx); + } + + @FieldResolver(() => WorkspaceOwnerComponent, { complexity: oneToManyComplexity, nullable: true }) + async getOwner(@Root() workspace: WorkspaceModel, @Ctx() ctx: ApolloContext): Promise { + if (workspace.createdBy) return await ctx.ctx.loaders.accountLoader.load(workspace.createdBy); + return; + } + + // @FieldResolver(() => [CommentModel], { complexity: oneToManyComplexity }) + // async comments(@Root() product: ProductModel, @Args() args: CommentArgs, @Ctx() ctx: ApolloContext): Promise<(CommentModel | Error)[]> { + // return (await ctx.ctx.loaders.commentRessourceLoader(args.pagination).load(product._id)) as any; + // } + + // // Todo: Optimize + // @FieldResolver(() => [ProductModel], { complexity: oneToManyComplexityNoLoader }) + // async relatedProducts(@Root() product: ProductModel, @Ctx() ctx: ApolloContext): Promise<(ProductModel | Error)[]> { + // return await product.getMany( + // { _id: { $ne: product._id }, categoryIds: { $in: product.categoryIds }, published: true }, + // { limit: 25, skip: 0 }, + // ctx, + // ); + // } +} diff --git a/src/workspace/workspace.resolver.ts b/src/workspace/workspace.resolver.ts new file mode 100644 index 0000000..817d2b0 --- /dev/null +++ b/src/workspace/workspace.resolver.ts @@ -0,0 +1,102 @@ +import { Arg, Args, Authorized, Ctx, Query, Resolver } from 'type-graphql'; + +import { createBaseResolver } from '@seed/graphql/baseResolvers/BaseResolver'; +import { AccountTypeEnum } from '@src/accounts/account.components'; +import WorkspaceModel, { WorkspaceArgs, EditWorkspaceInput, NewWorkspaceInput } from './workspace.model'; +import { getCountGeneric, getManyGenericWithArgs, getOneGeneric } from '@seed/graphql/BaseService'; +import { ApolloContext } from '@seed/interfaces/context'; +import { auth } from 'firebase-admin'; +import { domain } from 'process'; +import { BookingService } from '@services/module-booking/functions/bookings/booking.service'; +import { DateTime } from 'luxon'; +import { converDateRangeToDateTime, isTimeSlotInDates } from '@seed/helpers/Utils.dates'; +import { BookingsRessourceEnum } from '@src/__components/components'; +import moment from 'moment'; + +const WorkspaceBaseResolver = createBaseResolver('workspaces', WorkspaceModel, WorkspaceArgs, NewWorkspaceInput, EditWorkspaceInput, [ + AccountTypeEnum.user, +]); +@Resolver(WorkspaceModel) +export default class WorkspaceResolver extends WorkspaceBaseResolver { + /* + ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗ + ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝ + ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝ + ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝ + ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║ + ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ + */ + + @Query(() => WorkspaceModel) + async workspacesGetOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise { + const model = new WorkspaceModel(); + return getOneGeneric(model, id, ctx); + } + + @Query(() => [WorkspaceModel]) + async workspacesSearchMany(@Args() args: WorkspaceArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new WorkspaceModel(); + + // (1) Get the bookings in those dates + if (args.dateFrom && args.dateTo) { + const workspaceIdToOmit: any[] = await new BookingService().getBookingsToFilter(BookingsRessourceEnum.workspaces, { + startDate: args.dateFrom, + endDate: args.dateTo, + startTime: args.hoursFrom || '00:00', + endTime: args.hoursTo || '23:59', + }); + if (workspaceIdToOmit.length > 0) (args as any).n_ids = workspaceIdToOmit; + } + + // At least availability in the future + if (!args.dateFrom) (args as any).from = moment().toDate(); + + // (end) Get the workspaces without those above + const workspaces = await getManyGenericWithArgs(model, args, ctx); + return workspaces; + } + + @Query(() => Number) + async workspacesGetCount(@Args() args: WorkspaceArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new WorkspaceModel(); + + // (1) Get the bookings in those dates + if (args.dateFrom && args.dateTo) { + const workspaceIdToOmit: any[] = await new BookingService().getBookingsToFilter(BookingsRessourceEnum.workspaces, { + startDate: args.dateFrom, + endDate: args.dateTo, + startTime: args.hoursFrom || '00:00', + endTime: args.hoursTo || '23:59', + }); + if (workspaceIdToOmit.length > 0) (args as any).n_ids = workspaceIdToOmit; + } + + // At least availability in the future + if (!args.dateFrom) (args as any).from = moment().toDate(); + + // (end) Get the workspaces without those above + return getCountGeneric(model, args, ctx); + } + + @Query(() => [WorkspaceModel]) + @Authorized(AccountTypeEnum.user) + async workspacesGetMine(@Args() args: WorkspaceArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new WorkspaceModel(); + return getManyGenericWithArgs(model, { ...args, createdBy: ctx.ctx.user._id }, ctx); + } + @Query(() => Number) + @Authorized(AccountTypeEnum.user) + async workspacesGetMineCount(@Args() args: WorkspaceArgs, @Ctx() ctx: ApolloContext): Promise { + const model = new WorkspaceModel(); + return getCountGeneric(model, { ...args, createdBy: ctx.ctx.user._id }, ctx); + } + + /* + ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗ ██████╗ ██████╗ ███████╗ + ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ + ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║ ██║██████╔╝███████╗ + ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║ ██║██╔══██╗╚════██║ + ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████║ + ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + */ +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c09e529 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "preserveConstEnums": true, + "strictNullChecks": true, + "inlineSources": true, + "inlineSourceMap": true, + "sourceRoot": "/", + "target": "es2017", + "outDir": ".build", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "lib": ["es2020", "DOM", "DOM.Iterable"], + "rootDir": "./", + "baseUrl": "./", + "resolveJsonModule": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "paths": { + "@config/*": ["config/*"], + "@seed/*": ["lib/seed/*"], + "@src/*": ["src/*"], + "@services/*": ["services/*"], + "@lib/*": ["lib/*"], + + } + }, + "exclude": [ + "**/__tests/*", + "*.spec.ts", + "**/examples/*", + ] +}