diff --git a/apps/order-service-admin/.dockerignore b/apps/order-service-admin/.dockerignore new file mode 100644 index 0000000..1194b4f --- /dev/null +++ b/apps/order-service-admin/.dockerignore @@ -0,0 +1,7 @@ +.dockerignore +docker-compose.yml +Dockerfile +build/ +node_modules +.env +.gitignore diff --git a/apps/order-service-admin/.env b/apps/order-service-admin/.env new file mode 100644 index 0000000..e96bc90 --- /dev/null +++ b/apps/order-service-admin/.env @@ -0,0 +1,2 @@ +PORT=3001 +VITE_REACT_APP_SERVER_URL=http://localhost:3000 \ No newline at end of file diff --git a/apps/order-service-admin/.gitignore b/apps/order-service-admin/.gitignore new file mode 100644 index 0000000..590b2e0 --- /dev/null +++ b/apps/order-service-admin/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/apps/order-service-admin/Dockerfile b/apps/order-service-admin/Dockerfile new file mode 100644 index 0000000..0911e21 --- /dev/null +++ b/apps/order-service-admin/Dockerfile @@ -0,0 +1,51 @@ +# multi-stage: base (build) +FROM node:18.13.0-slim AS base + +# instantiate environment variable +ARG REACT_APP_SERVER_URL=http://localhost:3000 + +# set the environment variable that points to the server +ENV REACT_APP_SERVER_URL=$REACT_APP_SERVER_URL + +# create directory where the application will be built +WORKDIR /app + +# copy over the dependency manifests, both the package.json +# and the package-lock.json are copied over +COPY package*.json ./ + +# installs packages and their dependencies +RUN npm install + +# copy over the code base +COPY . . + +# create the bundle of the application +RUN npm run build + +# multi-stage: production (runtime) +FROM nginx:1.22-alpine AS production + +# copy over the bundled code from the build stage +COPY --from=base /app/build /usr/share/nginx/html +COPY --from=base /app/configuration/nginx.conf /etc/nginx/conf.d/default.conf + +# create a new process indication file +RUN touch /var/run/nginx.pid + +# change ownership of nginx related directories and files +RUN chown -R nginx:nginx /var/run/nginx.pid \ + /usr/share/nginx/html \ + /var/cache/nginx \ + /var/log/nginx \ + /etc/nginx/conf.d + +# set user to the created non-privileged user +USER nginx + +# expose a specific port on the docker container +ENV PORT=3001 +EXPOSE ${PORT} + +# start the server using the previously build application +ENTRYPOINT [ "nginx", "-g", "daemon off;" ] diff --git a/apps/order-service-admin/README.md b/apps/order-service-admin/README.md new file mode 100644 index 0000000..cc7c387 --- /dev/null +++ b/apps/order-service-admin/README.md @@ -0,0 +1,47 @@ +

+ + amplication-logo + +

+ +# Introduction + +This service was generated with Amplication. It serves as the client-side for the generated server component. The client-side consist of a React application with ready-made forms for creating and editing the different data models of the application. It is pre-conffigured to work with the server and comes with the boilerplate and foundation for the client - i.e., routing, navigation, authentication, permissions, menu, breadcrumbs, error handling and much more. Additional information about the admin component and the architecture around it, can be found on the [documentation](https://docs.amplication.com/guides/getting-started) site. This side of the generated project was bootstrapped with [create-react-app](https://github.com/facebook/create-react-app) and built with [react-admin](https://marmelab.com/react-admin/). + + +

+ +

+ +# Getting started + +## Step 1: Configuration + +Configuration for the client component can be provided through the use of environment variables. These can be passed to the application via the use of the `.env` file in the base directory of the generated service. Below a table can be found which show the different variables that can be passed. These values are provided default values after generation, change them to the desired values. + +| Variable | Description | Value | +| -------------------- | ------------------------------------------------ | ------------------------------ | +| PORT | the port on which to run the client | 3001 | +| REACT_APP_SERVER_URL | the url on which the server component is running | http://localhost:[server-port] | + +> **Note** +> Amplication generates default values and stores them under the .env file. It is advised to use some form of secrets manager/vault solution when using in production. + + +## Step 2: Scripts + +After configuration of the client the next step would be to run the application. Before running the client side of the component, make sure that the different pre-requisites are met - i.e., npm, docker. Make sure that the server-side of the application is running. + +```sh +# installation of the dependencies +$ npm install + +# starts the application in development mode - available by default under http://localhost:3001 with a pre-configured user with the username "admin" and password "admin" +$ npm run start + +# builds the application in production mode - available under 'build' +$ npm run build + +# removes the single build dependency from the project +$ npm run eject +``` diff --git a/apps/order-service-admin/configuration/nginx.conf b/apps/order-service-admin/configuration/nginx.conf new file mode 100644 index 0000000..c907b5c --- /dev/null +++ b/apps/order-service-admin/configuration/nginx.conf @@ -0,0 +1,11 @@ +server_tokens off; + +server { + listen 3001; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri /index.html; + } +} \ No newline at end of file diff --git a/apps/order-service-admin/index.html b/apps/order-service-admin/index.html new file mode 100644 index 0000000..6ba1449 --- /dev/null +++ b/apps/order-service-admin/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + Order Service + + + +
+ + + diff --git a/apps/order-service-admin/package.json b/apps/order-service-admin/package.json new file mode 100644 index 0000000..fbd0b7c --- /dev/null +++ b/apps/order-service-admin/package.json @@ -0,0 +1,41 @@ +{ + "name": "@order-service/admin", + "private": true, + "dependencies": { + "@apollo/client": "3.6.9", + "graphql": "15.6.1", + "lodash": "4.17.21", + "pluralize": "8.0.0", + "ra-data-graphql-amplication": "1.0.2", + "react": "^18.3.0", + "react-admin": "^5.1.0", + "react-dom": "^18.3.0", + "sass": "^1.39.0" + }, + "scripts": { + "start": "vite", + "build": "vite build", + "serve": "vite preview", + "type-check": "tsc --noEmit", + "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src", + "format": "prettier --write ./src", + "package:container": "docker build ." + }, + "devDependencies": { + "@types/lodash": "4.14.178", + "@types/node": "^20.10.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^5.60.1", + "@typescript-eslint/parser": "^5.60.1", + "@vitejs/plugin-react": "^4.0.1", + "eslint": "^8.43.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^2.8.8", + "type-fest": "0.13.1", + "typescript": "^5.1.6", + "vite": "^4.3.9" + } +} \ No newline at end of file diff --git a/apps/order-service-admin/public/favicon.ico b/apps/order-service-admin/public/favicon.ico new file mode 100644 index 0000000..fcbdcf2 Binary files /dev/null and b/apps/order-service-admin/public/favicon.ico differ diff --git a/apps/order-service-admin/public/logo192.png b/apps/order-service-admin/public/logo192.png new file mode 100644 index 0000000..1918ff2 Binary files /dev/null and b/apps/order-service-admin/public/logo192.png differ diff --git a/apps/order-service-admin/public/logo512.png b/apps/order-service-admin/public/logo512.png new file mode 100644 index 0000000..7e7dc74 Binary files /dev/null and b/apps/order-service-admin/public/logo512.png differ diff --git a/apps/order-service-admin/public/manifest.json b/apps/order-service-admin/public/manifest.json new file mode 100644 index 0000000..cc2c1ed --- /dev/null +++ b/apps/order-service-admin/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "Order Service", + "name": "Order Service", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} \ No newline at end of file diff --git a/apps/order-service-admin/public/robots.txt b/apps/order-service-admin/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/apps/order-service-admin/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/order-service-admin/src/App.scss b/apps/order-service-admin/src/App.scss new file mode 100644 index 0000000..4c1cbb0 --- /dev/null +++ b/apps/order-service-admin/src/App.scss @@ -0,0 +1,59 @@ +// .App { +// .MuiAppBar-colorSecondary { +// background-color: black; + +// .RaAppBar-menuButton-13 { +// background-color: yellow; +// } +// } + +// .MuiDrawer-paper { +// background-color: red; + +// .MuiListItemIcon-root { +// color: white; +// } +// } + +// .MuiButton-textPrimary { +// background-color: purple; +// margin: 0 0.5rem; +// color: white; +// padding: 0.5rem 1rem; + +// &:hover { +// background-color: blue; +// } +// } + +// .MuiTableRow-head { +// .MuiTableCell-head { +// background-color: black; +// color: white; +// } + +// .MuiTableSortLabel-root { +// &:hover { +// color: red; + +// .MuiTableSortLabel-icon { +// color: red !important; +// } +// } +// .MuiTableSortLabel-icon { +// color: white !important; +// } +// } +// .MuiTableSortLabel-active { +// color: green; + +// .MuiTableSortLabel-icon { +// color: green !important; +// } +// } +// } + +// .MuiFormLabel-root { +// color: magenta; +// } +// } diff --git a/apps/order-service-admin/src/App.tsx b/apps/order-service-admin/src/App.tsx new file mode 100644 index 0000000..4da92be --- /dev/null +++ b/apps/order-service-admin/src/App.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from "react"; +import { Admin, DataProvider, Resource } from "react-admin"; +import dataProvider from "./data-provider/graphqlDataProvider"; +import { theme } from "./theme/theme"; +import Login from "./Login"; +import "./App.scss"; +import Dashboard from "./pages/Dashboard"; +import { UserList } from "./user/UserList"; +import { UserCreate } from "./user/UserCreate"; +import { UserEdit } from "./user/UserEdit"; +import { UserShow } from "./user/UserShow"; +import { OrderList } from "./order/OrderList"; +import { OrderCreate } from "./order/OrderCreate"; +import { OrderEdit } from "./order/OrderEdit"; +import { OrderShow } from "./order/OrderShow"; +import { CustomerList } from "./customer/CustomerList"; +import { CustomerCreate } from "./customer/CustomerCreate"; +import { CustomerEdit } from "./customer/CustomerEdit"; +import { CustomerShow } from "./customer/CustomerShow"; +import { AddressList } from "./address/AddressList"; +import { AddressCreate } from "./address/AddressCreate"; +import { AddressEdit } from "./address/AddressEdit"; +import { AddressShow } from "./address/AddressShow"; +import { ProductList } from "./product/ProductList"; +import { ProductCreate } from "./product/ProductCreate"; +import { ProductEdit } from "./product/ProductEdit"; +import { ProductShow } from "./product/ProductShow"; +import { jwtAuthProvider } from "./auth-provider/ra-auth-jwt"; + +const App = (): React.ReactElement => { + return ( +
+ + + + + + + +
+ ); +}; + +export default App; diff --git a/apps/order-service-admin/src/Components/Pagination.tsx b/apps/order-service-admin/src/Components/Pagination.tsx new file mode 100644 index 0000000..2de2ebf --- /dev/null +++ b/apps/order-service-admin/src/Components/Pagination.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { Pagination as RAPagination, PaginationProps } from "react-admin"; + +const PAGINATION_OPTIONS = [10, 25, 50, 100, 200]; + +const Pagination = (props: PaginationProps) => ( + +); + +export default Pagination; diff --git a/apps/order-service-admin/src/Login.tsx b/apps/order-service-admin/src/Login.tsx new file mode 100644 index 0000000..dbd6a85 --- /dev/null +++ b/apps/order-service-admin/src/Login.tsx @@ -0,0 +1,82 @@ +import * as React from "react"; +import { useState } from "react"; +import { useLogin, useNotify, Notification, defaultTheme } from "react-admin"; +import { Button, createTheme, ThemeProvider } from "@mui/material"; +import "./login.scss"; +import LoginForm from "./LoginForm"; + +const CLASS_NAME = "login-page"; + +const Login = ({ theme }: any) => { + const BASE_URI = process.env.REACT_APP_SERVER_URL; + + return ( + +
+
+
+ GraphQL API +

Connect via GraphQL

+
+ Connect to the server using GraphQL API with a complete and + understandable description of the data in your API +
+ +
+
+ React-Admin +

Admin UI

+
+ Sign in to a React-Admin client with ready-made forms for creating + and editing all the data models of your application +
+ +
+
+ REST API +

Connect via REST API

+
+ Connect to the server using REST API with a built-in Swagger + documentation +
+ +
+ + +
+
+ Read + + Amplication docs + + to learn more +
+
+
+ ); +}; + +export default Login; diff --git a/apps/order-service-admin/src/LoginForm.tsx b/apps/order-service-admin/src/LoginForm.tsx new file mode 100644 index 0000000..92d48da --- /dev/null +++ b/apps/order-service-admin/src/LoginForm.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { useLogin, useNotify } from "react-admin"; +import { Button } from "@mui/material"; +import "./login.scss"; + +const LoginForm = ({ theme }: any) => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const login = useLogin(); + const notify = useNotify(); + const BASE_URI = process.env.REACT_APP_SERVER_URL; + const submit = (e: any) => { + e.preventDefault(); + login({ username, password }).catch(() => + notify("Invalid username or password") + ); + }; + + return ( +
+ + + +
+ ); +}; + +export default LoginForm; diff --git a/apps/order-service-admin/src/address/AddressCreate.tsx b/apps/order-service-admin/src/address/AddressCreate.tsx new file mode 100644 index 0000000..8340df0 --- /dev/null +++ b/apps/order-service-admin/src/address/AddressCreate.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; + +import { + Create, + SimpleForm, + CreateProps, + TextInput, + ReferenceInput, + SelectInput, + NumberInput, +} from "react-admin"; + +import { CustomerTitle } from "../customer/CustomerTitle"; + +export const AddressCreate = (props: CreateProps): React.ReactElement => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/address/AddressEdit.tsx b/apps/order-service-admin/src/address/AddressEdit.tsx new file mode 100644 index 0000000..410cc95 --- /dev/null +++ b/apps/order-service-admin/src/address/AddressEdit.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; + +import { + Edit, + SimpleForm, + EditProps, + TextInput, + ReferenceInput, + SelectInput, + NumberInput, +} from "react-admin"; + +import { CustomerTitle } from "../customer/CustomerTitle"; + +export const AddressEdit = (props: EditProps): React.ReactElement => { + return ( + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/address/AddressList.tsx b/apps/order-service-admin/src/address/AddressList.tsx new file mode 100644 index 0000000..180e44c --- /dev/null +++ b/apps/order-service-admin/src/address/AddressList.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import { + List, + Datagrid, + ListProps, + TextField, + DateField, + ReferenceField, +} from "react-admin"; +import Pagination from "../Components/Pagination"; +import { CUSTOMER_TITLE_FIELD } from "../customer/CustomerTitle"; + +export const AddressList = (props: ListProps): React.ReactElement => { + return ( + } + > + + + + + + + + + + + + {" "} + + + ); +}; diff --git a/apps/order-service-admin/src/address/AddressShow.tsx b/apps/order-service-admin/src/address/AddressShow.tsx new file mode 100644 index 0000000..f92b7f6 --- /dev/null +++ b/apps/order-service-admin/src/address/AddressShow.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { + Show, + SimpleShowLayout, + ShowProps, + TextField, + DateField, + ReferenceField, +} from "react-admin"; +import { CUSTOMER_TITLE_FIELD } from "../customer/CustomerTitle"; + +export const AddressShow = (props: ShowProps): React.ReactElement => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/address/AddressTitle.ts b/apps/order-service-admin/src/address/AddressTitle.ts new file mode 100644 index 0000000..eae17ed --- /dev/null +++ b/apps/order-service-admin/src/address/AddressTitle.ts @@ -0,0 +1,7 @@ +import { Address as TAddress } from "../api/address/Address"; + +export const ADDRESS_TITLE_FIELD = "address_1"; + +export const AddressTitle = (record: TAddress): string => { + return record.address_1?.toString() || String(record.id); +}; diff --git a/apps/order-service-admin/src/api/address/Address.ts b/apps/order-service-admin/src/api/address/Address.ts new file mode 100644 index 0000000..4625205 --- /dev/null +++ b/apps/order-service-admin/src/api/address/Address.ts @@ -0,0 +1,13 @@ +import { Customer } from "../customer/Customer"; + +export type Address = { + address_1: string | null; + address_2: string | null; + city: string | null; + createdAt: Date; + customers?: Customer | null; + id: string; + state: string | null; + updatedAt: Date; + zip: number | null; +}; diff --git a/apps/order-service-admin/src/api/address/AddressCountArgs.ts b/apps/order-service-admin/src/api/address/AddressCountArgs.ts new file mode 100644 index 0000000..30aba6c --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressCountArgs.ts @@ -0,0 +1,5 @@ +import { AddressWhereInput } from "./AddressWhereInput"; + +export type AddressCountArgs = { + where?: AddressWhereInput; +}; diff --git a/apps/order-service-admin/src/api/address/AddressCreateInput.ts b/apps/order-service-admin/src/api/address/AddressCreateInput.ts new file mode 100644 index 0000000..a8b8004 --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressCreateInput.ts @@ -0,0 +1,10 @@ +import { CustomerWhereUniqueInput } from "../customer/CustomerWhereUniqueInput"; + +export type AddressCreateInput = { + address_1?: string | null; + address_2?: string | null; + city?: string | null; + customers?: CustomerWhereUniqueInput | null; + state?: string | null; + zip?: number | null; +}; diff --git a/apps/order-service-admin/src/api/address/AddressFindManyArgs.ts b/apps/order-service-admin/src/api/address/AddressFindManyArgs.ts new file mode 100644 index 0000000..459afb2 --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressFindManyArgs.ts @@ -0,0 +1,9 @@ +import { AddressWhereInput } from "./AddressWhereInput"; +import { AddressOrderByInput } from "./AddressOrderByInput"; + +export type AddressFindManyArgs = { + where?: AddressWhereInput; + orderBy?: Array; + skip?: number; + take?: number; +}; diff --git a/apps/order-service-admin/src/api/address/AddressFindUniqueArgs.ts b/apps/order-service-admin/src/api/address/AddressFindUniqueArgs.ts new file mode 100644 index 0000000..42f263f --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressFindUniqueArgs.ts @@ -0,0 +1,5 @@ +import { AddressWhereUniqueInput } from "./AddressWhereUniqueInput"; + +export type AddressFindUniqueArgs = { + where: AddressWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/address/AddressListRelationFilter.ts b/apps/order-service-admin/src/api/address/AddressListRelationFilter.ts new file mode 100644 index 0000000..7a29026 --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressListRelationFilter.ts @@ -0,0 +1,7 @@ +import { AddressWhereInput } from "./AddressWhereInput"; + +export type AddressListRelationFilter = { + every?: AddressWhereInput; + some?: AddressWhereInput; + none?: AddressWhereInput; +}; diff --git a/apps/order-service-admin/src/api/address/AddressOrderByInput.ts b/apps/order-service-admin/src/api/address/AddressOrderByInput.ts new file mode 100644 index 0000000..fb198fd --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressOrderByInput.ts @@ -0,0 +1,13 @@ +import { SortOrder } from "../../util/SortOrder"; + +export type AddressOrderByInput = { + address_1?: SortOrder; + address_2?: SortOrder; + city?: SortOrder; + createdAt?: SortOrder; + customersId?: SortOrder; + id?: SortOrder; + state?: SortOrder; + updatedAt?: SortOrder; + zip?: SortOrder; +}; diff --git a/apps/order-service-admin/src/api/address/AddressUpdateInput.ts b/apps/order-service-admin/src/api/address/AddressUpdateInput.ts new file mode 100644 index 0000000..232101d --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressUpdateInput.ts @@ -0,0 +1,10 @@ +import { CustomerWhereUniqueInput } from "../customer/CustomerWhereUniqueInput"; + +export type AddressUpdateInput = { + address_1?: string | null; + address_2?: string | null; + city?: string | null; + customers?: CustomerWhereUniqueInput | null; + state?: string | null; + zip?: number | null; +}; diff --git a/apps/order-service-admin/src/api/address/AddressWhereInput.ts b/apps/order-service-admin/src/api/address/AddressWhereInput.ts new file mode 100644 index 0000000..1a08fe7 --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressWhereInput.ts @@ -0,0 +1,14 @@ +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { CustomerWhereUniqueInput } from "../customer/CustomerWhereUniqueInput"; +import { StringFilter } from "../../util/StringFilter"; +import { IntNullableFilter } from "../../util/IntNullableFilter"; + +export type AddressWhereInput = { + address_1?: StringNullableFilter; + address_2?: StringNullableFilter; + city?: StringNullableFilter; + customers?: CustomerWhereUniqueInput; + id?: StringFilter; + state?: StringNullableFilter; + zip?: IntNullableFilter; +}; diff --git a/apps/order-service-admin/src/api/address/AddressWhereUniqueInput.ts b/apps/order-service-admin/src/api/address/AddressWhereUniqueInput.ts new file mode 100644 index 0000000..1eb84ac --- /dev/null +++ b/apps/order-service-admin/src/api/address/AddressWhereUniqueInput.ts @@ -0,0 +1,3 @@ +export type AddressWhereUniqueInput = { + id: string; +}; diff --git a/apps/order-service-admin/src/api/address/CreateAddressArgs.ts b/apps/order-service-admin/src/api/address/CreateAddressArgs.ts new file mode 100644 index 0000000..ca4273a --- /dev/null +++ b/apps/order-service-admin/src/api/address/CreateAddressArgs.ts @@ -0,0 +1,5 @@ +import { AddressCreateInput } from "./AddressCreateInput"; + +export type CreateAddressArgs = { + data: AddressCreateInput; +}; diff --git a/apps/order-service-admin/src/api/address/DeleteAddressArgs.ts b/apps/order-service-admin/src/api/address/DeleteAddressArgs.ts new file mode 100644 index 0000000..14f1650 --- /dev/null +++ b/apps/order-service-admin/src/api/address/DeleteAddressArgs.ts @@ -0,0 +1,5 @@ +import { AddressWhereUniqueInput } from "./AddressWhereUniqueInput"; + +export type DeleteAddressArgs = { + where: AddressWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/address/UpdateAddressArgs.ts b/apps/order-service-admin/src/api/address/UpdateAddressArgs.ts new file mode 100644 index 0000000..181144d --- /dev/null +++ b/apps/order-service-admin/src/api/address/UpdateAddressArgs.ts @@ -0,0 +1,7 @@ +import { AddressWhereUniqueInput } from "./AddressWhereUniqueInput"; +import { AddressUpdateInput } from "./AddressUpdateInput"; + +export type UpdateAddressArgs = { + where: AddressWhereUniqueInput; + data: AddressUpdateInput; +}; diff --git a/apps/order-service-admin/src/api/customer/CreateCustomerArgs.ts b/apps/order-service-admin/src/api/customer/CreateCustomerArgs.ts new file mode 100644 index 0000000..216376d --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CreateCustomerArgs.ts @@ -0,0 +1,5 @@ +import { CustomerCreateInput } from "./CustomerCreateInput"; + +export type CreateCustomerArgs = { + data: CustomerCreateInput; +}; diff --git a/apps/order-service-admin/src/api/customer/Customer.ts b/apps/order-service-admin/src/api/customer/Customer.ts new file mode 100644 index 0000000..28abe76 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/Customer.ts @@ -0,0 +1,14 @@ +import { Address } from "../address/Address"; +import { Order } from "../order/Order"; + +export type Customer = { + address?: Address | null; + createdAt: Date; + email: string | null; + firstName: string | null; + id: string; + lastName: string | null; + orders?: Order | null; + phone: string | null; + updatedAt: Date; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerCountArgs.ts b/apps/order-service-admin/src/api/customer/CustomerCountArgs.ts new file mode 100644 index 0000000..18fe18e --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerCountArgs.ts @@ -0,0 +1,5 @@ +import { CustomerWhereInput } from "./CustomerWhereInput"; + +export type CustomerCountArgs = { + where?: CustomerWhereInput; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerCreateInput.ts b/apps/order-service-admin/src/api/customer/CustomerCreateInput.ts new file mode 100644 index 0000000..143ca19 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerCreateInput.ts @@ -0,0 +1,11 @@ +import { AddressWhereUniqueInput } from "../address/AddressWhereUniqueInput"; +import { OrderWhereUniqueInput } from "../order/OrderWhereUniqueInput"; + +export type CustomerCreateInput = { + address?: AddressWhereUniqueInput | null; + email?: string | null; + firstName?: string | null; + lastName?: string | null; + orders?: OrderWhereUniqueInput | null; + phone?: string | null; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerFindManyArgs.ts b/apps/order-service-admin/src/api/customer/CustomerFindManyArgs.ts new file mode 100644 index 0000000..e1e7b0a --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerFindManyArgs.ts @@ -0,0 +1,9 @@ +import { CustomerWhereInput } from "./CustomerWhereInput"; +import { CustomerOrderByInput } from "./CustomerOrderByInput"; + +export type CustomerFindManyArgs = { + where?: CustomerWhereInput; + orderBy?: Array; + skip?: number; + take?: number; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerFindUniqueArgs.ts b/apps/order-service-admin/src/api/customer/CustomerFindUniqueArgs.ts new file mode 100644 index 0000000..c0b9e44 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerFindUniqueArgs.ts @@ -0,0 +1,5 @@ +import { CustomerWhereUniqueInput } from "./CustomerWhereUniqueInput"; + +export type CustomerFindUniqueArgs = { + where: CustomerWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerListRelationFilter.ts b/apps/order-service-admin/src/api/customer/CustomerListRelationFilter.ts new file mode 100644 index 0000000..64514f8 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerListRelationFilter.ts @@ -0,0 +1,7 @@ +import { CustomerWhereInput } from "./CustomerWhereInput"; + +export type CustomerListRelationFilter = { + every?: CustomerWhereInput; + some?: CustomerWhereInput; + none?: CustomerWhereInput; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerOrderByInput.ts b/apps/order-service-admin/src/api/customer/CustomerOrderByInput.ts new file mode 100644 index 0000000..6add3a1 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerOrderByInput.ts @@ -0,0 +1,13 @@ +import { SortOrder } from "../../util/SortOrder"; + +export type CustomerOrderByInput = { + addressId?: SortOrder; + createdAt?: SortOrder; + email?: SortOrder; + firstName?: SortOrder; + id?: SortOrder; + lastName?: SortOrder; + ordersId?: SortOrder; + phone?: SortOrder; + updatedAt?: SortOrder; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerUpdateInput.ts b/apps/order-service-admin/src/api/customer/CustomerUpdateInput.ts new file mode 100644 index 0000000..a501f36 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerUpdateInput.ts @@ -0,0 +1,11 @@ +import { AddressWhereUniqueInput } from "../address/AddressWhereUniqueInput"; +import { OrderWhereUniqueInput } from "../order/OrderWhereUniqueInput"; + +export type CustomerUpdateInput = { + address?: AddressWhereUniqueInput | null; + email?: string | null; + firstName?: string | null; + lastName?: string | null; + orders?: OrderWhereUniqueInput | null; + phone?: string | null; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerWhereInput.ts b/apps/order-service-admin/src/api/customer/CustomerWhereInput.ts new file mode 100644 index 0000000..34b7853 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerWhereInput.ts @@ -0,0 +1,14 @@ +import { AddressWhereUniqueInput } from "../address/AddressWhereUniqueInput"; +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { StringFilter } from "../../util/StringFilter"; +import { OrderWhereUniqueInput } from "../order/OrderWhereUniqueInput"; + +export type CustomerWhereInput = { + address?: AddressWhereUniqueInput; + email?: StringNullableFilter; + firstName?: StringNullableFilter; + id?: StringFilter; + lastName?: StringNullableFilter; + orders?: OrderWhereUniqueInput; + phone?: StringNullableFilter; +}; diff --git a/apps/order-service-admin/src/api/customer/CustomerWhereUniqueInput.ts b/apps/order-service-admin/src/api/customer/CustomerWhereUniqueInput.ts new file mode 100644 index 0000000..87b1d6e --- /dev/null +++ b/apps/order-service-admin/src/api/customer/CustomerWhereUniqueInput.ts @@ -0,0 +1,3 @@ +export type CustomerWhereUniqueInput = { + id: string; +}; diff --git a/apps/order-service-admin/src/api/customer/DeleteCustomerArgs.ts b/apps/order-service-admin/src/api/customer/DeleteCustomerArgs.ts new file mode 100644 index 0000000..64bdd7b --- /dev/null +++ b/apps/order-service-admin/src/api/customer/DeleteCustomerArgs.ts @@ -0,0 +1,5 @@ +import { CustomerWhereUniqueInput } from "./CustomerWhereUniqueInput"; + +export type DeleteCustomerArgs = { + where: CustomerWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/customer/UpdateCustomerArgs.ts b/apps/order-service-admin/src/api/customer/UpdateCustomerArgs.ts new file mode 100644 index 0000000..65a7b32 --- /dev/null +++ b/apps/order-service-admin/src/api/customer/UpdateCustomerArgs.ts @@ -0,0 +1,7 @@ +import { CustomerWhereUniqueInput } from "./CustomerWhereUniqueInput"; +import { CustomerUpdateInput } from "./CustomerUpdateInput"; + +export type UpdateCustomerArgs = { + where: CustomerWhereUniqueInput; + data: CustomerUpdateInput; +}; diff --git a/apps/order-service-admin/src/api/order/CreateOrderArgs.ts b/apps/order-service-admin/src/api/order/CreateOrderArgs.ts new file mode 100644 index 0000000..ae92a61 --- /dev/null +++ b/apps/order-service-admin/src/api/order/CreateOrderArgs.ts @@ -0,0 +1,5 @@ +import { OrderCreateInput } from "./OrderCreateInput"; + +export type CreateOrderArgs = { + data: OrderCreateInput; +}; diff --git a/apps/order-service-admin/src/api/order/DeleteOrderArgs.ts b/apps/order-service-admin/src/api/order/DeleteOrderArgs.ts new file mode 100644 index 0000000..72c9a43 --- /dev/null +++ b/apps/order-service-admin/src/api/order/DeleteOrderArgs.ts @@ -0,0 +1,5 @@ +import { OrderWhereUniqueInput } from "./OrderWhereUniqueInput"; + +export type DeleteOrderArgs = { + where: OrderWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/order/Order.ts b/apps/order-service-admin/src/api/order/Order.ts new file mode 100644 index 0000000..6216bd3 --- /dev/null +++ b/apps/order-service-admin/src/api/order/Order.ts @@ -0,0 +1,13 @@ +import { Customer } from "../customer/Customer"; +import { Product } from "../product/Product"; + +export type Order = { + createdAt: Date; + customer?: Customer | null; + discount: number | null; + id: string; + product?: Product | null; + quantity: number | null; + totalPrice: number | null; + updatedAt: Date; +}; diff --git a/apps/order-service-admin/src/api/order/OrderCountArgs.ts b/apps/order-service-admin/src/api/order/OrderCountArgs.ts new file mode 100644 index 0000000..0671694 --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderCountArgs.ts @@ -0,0 +1,5 @@ +import { OrderWhereInput } from "./OrderWhereInput"; + +export type OrderCountArgs = { + where?: OrderWhereInput; +}; diff --git a/apps/order-service-admin/src/api/order/OrderCreateInput.ts b/apps/order-service-admin/src/api/order/OrderCreateInput.ts new file mode 100644 index 0000000..991131d --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderCreateInput.ts @@ -0,0 +1,10 @@ +import { CustomerWhereUniqueInput } from "../customer/CustomerWhereUniqueInput"; +import { ProductWhereUniqueInput } from "../product/ProductWhereUniqueInput"; + +export type OrderCreateInput = { + customer?: CustomerWhereUniqueInput | null; + discount?: number | null; + product?: ProductWhereUniqueInput | null; + quantity?: number | null; + totalPrice?: number | null; +}; diff --git a/apps/order-service-admin/src/api/order/OrderFindManyArgs.ts b/apps/order-service-admin/src/api/order/OrderFindManyArgs.ts new file mode 100644 index 0000000..44e4a43 --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderFindManyArgs.ts @@ -0,0 +1,9 @@ +import { OrderWhereInput } from "./OrderWhereInput"; +import { OrderOrderByInput } from "./OrderOrderByInput"; + +export type OrderFindManyArgs = { + where?: OrderWhereInput; + orderBy?: Array; + skip?: number; + take?: number; +}; diff --git a/apps/order-service-admin/src/api/order/OrderFindUniqueArgs.ts b/apps/order-service-admin/src/api/order/OrderFindUniqueArgs.ts new file mode 100644 index 0000000..3be5d14 --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderFindUniqueArgs.ts @@ -0,0 +1,5 @@ +import { OrderWhereUniqueInput } from "./OrderWhereUniqueInput"; + +export type OrderFindUniqueArgs = { + where: OrderWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/order/OrderListRelationFilter.ts b/apps/order-service-admin/src/api/order/OrderListRelationFilter.ts new file mode 100644 index 0000000..c9b2856 --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderListRelationFilter.ts @@ -0,0 +1,7 @@ +import { OrderWhereInput } from "./OrderWhereInput"; + +export type OrderListRelationFilter = { + every?: OrderWhereInput; + some?: OrderWhereInput; + none?: OrderWhereInput; +}; diff --git a/apps/order-service-admin/src/api/order/OrderOrderByInput.ts b/apps/order-service-admin/src/api/order/OrderOrderByInput.ts new file mode 100644 index 0000000..8a68733 --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderOrderByInput.ts @@ -0,0 +1,12 @@ +import { SortOrder } from "../../util/SortOrder"; + +export type OrderOrderByInput = { + createdAt?: SortOrder; + customerId?: SortOrder; + discount?: SortOrder; + id?: SortOrder; + productId?: SortOrder; + quantity?: SortOrder; + totalPrice?: SortOrder; + updatedAt?: SortOrder; +}; diff --git a/apps/order-service-admin/src/api/order/OrderUpdateInput.ts b/apps/order-service-admin/src/api/order/OrderUpdateInput.ts new file mode 100644 index 0000000..87fc1dc --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderUpdateInput.ts @@ -0,0 +1,10 @@ +import { CustomerWhereUniqueInput } from "../customer/CustomerWhereUniqueInput"; +import { ProductWhereUniqueInput } from "../product/ProductWhereUniqueInput"; + +export type OrderUpdateInput = { + customer?: CustomerWhereUniqueInput | null; + discount?: number | null; + product?: ProductWhereUniqueInput | null; + quantity?: number | null; + totalPrice?: number | null; +}; diff --git a/apps/order-service-admin/src/api/order/OrderWhereInput.ts b/apps/order-service-admin/src/api/order/OrderWhereInput.ts new file mode 100644 index 0000000..5d5d895 --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderWhereInput.ts @@ -0,0 +1,14 @@ +import { CustomerWhereUniqueInput } from "../customer/CustomerWhereUniqueInput"; +import { FloatNullableFilter } from "../../util/FloatNullableFilter"; +import { StringFilter } from "../../util/StringFilter"; +import { ProductWhereUniqueInput } from "../product/ProductWhereUniqueInput"; +import { IntNullableFilter } from "../../util/IntNullableFilter"; + +export type OrderWhereInput = { + customer?: CustomerWhereUniqueInput; + discount?: FloatNullableFilter; + id?: StringFilter; + product?: ProductWhereUniqueInput; + quantity?: IntNullableFilter; + totalPrice?: IntNullableFilter; +}; diff --git a/apps/order-service-admin/src/api/order/OrderWhereUniqueInput.ts b/apps/order-service-admin/src/api/order/OrderWhereUniqueInput.ts new file mode 100644 index 0000000..ea2ede7 --- /dev/null +++ b/apps/order-service-admin/src/api/order/OrderWhereUniqueInput.ts @@ -0,0 +1,3 @@ +export type OrderWhereUniqueInput = { + id: string; +}; diff --git a/apps/order-service-admin/src/api/order/UpdateOrderArgs.ts b/apps/order-service-admin/src/api/order/UpdateOrderArgs.ts new file mode 100644 index 0000000..88d849f --- /dev/null +++ b/apps/order-service-admin/src/api/order/UpdateOrderArgs.ts @@ -0,0 +1,7 @@ +import { OrderWhereUniqueInput } from "./OrderWhereUniqueInput"; +import { OrderUpdateInput } from "./OrderUpdateInput"; + +export type UpdateOrderArgs = { + where: OrderWhereUniqueInput; + data: OrderUpdateInput; +}; diff --git a/apps/order-service-admin/src/api/product/CreateProductArgs.ts b/apps/order-service-admin/src/api/product/CreateProductArgs.ts new file mode 100644 index 0000000..3e98f36 --- /dev/null +++ b/apps/order-service-admin/src/api/product/CreateProductArgs.ts @@ -0,0 +1,5 @@ +import { ProductCreateInput } from "./ProductCreateInput"; + +export type CreateProductArgs = { + data: ProductCreateInput; +}; diff --git a/apps/order-service-admin/src/api/product/DeleteProductArgs.ts b/apps/order-service-admin/src/api/product/DeleteProductArgs.ts new file mode 100644 index 0000000..f1f7ba1 --- /dev/null +++ b/apps/order-service-admin/src/api/product/DeleteProductArgs.ts @@ -0,0 +1,5 @@ +import { ProductWhereUniqueInput } from "./ProductWhereUniqueInput"; + +export type DeleteProductArgs = { + where: ProductWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/product/Product.ts b/apps/order-service-admin/src/api/product/Product.ts new file mode 100644 index 0000000..b5a48e4 --- /dev/null +++ b/apps/order-service-admin/src/api/product/Product.ts @@ -0,0 +1,11 @@ +import { Order } from "../order/Order"; + +export type Product = { + createdAt: Date; + description: string | null; + id: string; + itemPrice: number | null; + name: string | null; + orders?: Order | null; + updatedAt: Date; +}; diff --git a/apps/order-service-admin/src/api/product/ProductCountArgs.ts b/apps/order-service-admin/src/api/product/ProductCountArgs.ts new file mode 100644 index 0000000..e0bb130 --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductCountArgs.ts @@ -0,0 +1,5 @@ +import { ProductWhereInput } from "./ProductWhereInput"; + +export type ProductCountArgs = { + where?: ProductWhereInput; +}; diff --git a/apps/order-service-admin/src/api/product/ProductCreateInput.ts b/apps/order-service-admin/src/api/product/ProductCreateInput.ts new file mode 100644 index 0000000..b276ba1 --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductCreateInput.ts @@ -0,0 +1,8 @@ +import { OrderWhereUniqueInput } from "../order/OrderWhereUniqueInput"; + +export type ProductCreateInput = { + description?: string | null; + itemPrice?: number | null; + name?: string | null; + orders?: OrderWhereUniqueInput | null; +}; diff --git a/apps/order-service-admin/src/api/product/ProductFindManyArgs.ts b/apps/order-service-admin/src/api/product/ProductFindManyArgs.ts new file mode 100644 index 0000000..bcaae8a --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductFindManyArgs.ts @@ -0,0 +1,9 @@ +import { ProductWhereInput } from "./ProductWhereInput"; +import { ProductOrderByInput } from "./ProductOrderByInput"; + +export type ProductFindManyArgs = { + where?: ProductWhereInput; + orderBy?: Array; + skip?: number; + take?: number; +}; diff --git a/apps/order-service-admin/src/api/product/ProductFindUniqueArgs.ts b/apps/order-service-admin/src/api/product/ProductFindUniqueArgs.ts new file mode 100644 index 0000000..5f6b0f6 --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductFindUniqueArgs.ts @@ -0,0 +1,5 @@ +import { ProductWhereUniqueInput } from "./ProductWhereUniqueInput"; + +export type ProductFindUniqueArgs = { + where: ProductWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/product/ProductListRelationFilter.ts b/apps/order-service-admin/src/api/product/ProductListRelationFilter.ts new file mode 100644 index 0000000..e1c37ca --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductListRelationFilter.ts @@ -0,0 +1,7 @@ +import { ProductWhereInput } from "./ProductWhereInput"; + +export type ProductListRelationFilter = { + every?: ProductWhereInput; + some?: ProductWhereInput; + none?: ProductWhereInput; +}; diff --git a/apps/order-service-admin/src/api/product/ProductOrderByInput.ts b/apps/order-service-admin/src/api/product/ProductOrderByInput.ts new file mode 100644 index 0000000..4cce902 --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductOrderByInput.ts @@ -0,0 +1,11 @@ +import { SortOrder } from "../../util/SortOrder"; + +export type ProductOrderByInput = { + createdAt?: SortOrder; + description?: SortOrder; + id?: SortOrder; + itemPrice?: SortOrder; + name?: SortOrder; + ordersId?: SortOrder; + updatedAt?: SortOrder; +}; diff --git a/apps/order-service-admin/src/api/product/ProductUpdateInput.ts b/apps/order-service-admin/src/api/product/ProductUpdateInput.ts new file mode 100644 index 0000000..7ce0cc0 --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductUpdateInput.ts @@ -0,0 +1,8 @@ +import { OrderWhereUniqueInput } from "../order/OrderWhereUniqueInput"; + +export type ProductUpdateInput = { + description?: string | null; + itemPrice?: number | null; + name?: string | null; + orders?: OrderWhereUniqueInput | null; +}; diff --git a/apps/order-service-admin/src/api/product/ProductWhereInput.ts b/apps/order-service-admin/src/api/product/ProductWhereInput.ts new file mode 100644 index 0000000..c92b715 --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductWhereInput.ts @@ -0,0 +1,12 @@ +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { StringFilter } from "../../util/StringFilter"; +import { FloatNullableFilter } from "../../util/FloatNullableFilter"; +import { OrderWhereUniqueInput } from "../order/OrderWhereUniqueInput"; + +export type ProductWhereInput = { + description?: StringNullableFilter; + id?: StringFilter; + itemPrice?: FloatNullableFilter; + name?: StringNullableFilter; + orders?: OrderWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/product/ProductWhereUniqueInput.ts b/apps/order-service-admin/src/api/product/ProductWhereUniqueInput.ts new file mode 100644 index 0000000..3432b3b --- /dev/null +++ b/apps/order-service-admin/src/api/product/ProductWhereUniqueInput.ts @@ -0,0 +1,3 @@ +export type ProductWhereUniqueInput = { + id: string; +}; diff --git a/apps/order-service-admin/src/api/product/UpdateProductArgs.ts b/apps/order-service-admin/src/api/product/UpdateProductArgs.ts new file mode 100644 index 0000000..54fba33 --- /dev/null +++ b/apps/order-service-admin/src/api/product/UpdateProductArgs.ts @@ -0,0 +1,7 @@ +import { ProductWhereUniqueInput } from "./ProductWhereUniqueInput"; +import { ProductUpdateInput } from "./ProductUpdateInput"; + +export type UpdateProductArgs = { + where: ProductWhereUniqueInput; + data: ProductUpdateInput; +}; diff --git a/apps/order-service-admin/src/api/user/CreateUserArgs.ts b/apps/order-service-admin/src/api/user/CreateUserArgs.ts new file mode 100644 index 0000000..2a56f0c --- /dev/null +++ b/apps/order-service-admin/src/api/user/CreateUserArgs.ts @@ -0,0 +1,5 @@ +import { UserCreateInput } from "./UserCreateInput"; + +export type CreateUserArgs = { + data: UserCreateInput; +}; diff --git a/apps/order-service-admin/src/api/user/DeleteUserArgs.ts b/apps/order-service-admin/src/api/user/DeleteUserArgs.ts new file mode 100644 index 0000000..5f655b8 --- /dev/null +++ b/apps/order-service-admin/src/api/user/DeleteUserArgs.ts @@ -0,0 +1,5 @@ +import { UserWhereUniqueInput } from "./UserWhereUniqueInput"; + +export type DeleteUserArgs = { + where: UserWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/user/UpdateUserArgs.ts b/apps/order-service-admin/src/api/user/UpdateUserArgs.ts new file mode 100644 index 0000000..30e635e --- /dev/null +++ b/apps/order-service-admin/src/api/user/UpdateUserArgs.ts @@ -0,0 +1,7 @@ +import { UserWhereUniqueInput } from "./UserWhereUniqueInput"; +import { UserUpdateInput } from "./UserUpdateInput"; + +export type UpdateUserArgs = { + where: UserWhereUniqueInput; + data: UserUpdateInput; +}; diff --git a/apps/order-service-admin/src/api/user/User.ts b/apps/order-service-admin/src/api/user/User.ts new file mode 100644 index 0000000..9c53564 --- /dev/null +++ b/apps/order-service-admin/src/api/user/User.ts @@ -0,0 +1,11 @@ +import { JsonValue } from "type-fest"; + +export type User = { + createdAt: Date; + firstName: string | null; + id: string; + lastName: string | null; + roles: JsonValue; + updatedAt: Date; + username: string; +}; diff --git a/apps/order-service-admin/src/api/user/UserCountArgs.ts b/apps/order-service-admin/src/api/user/UserCountArgs.ts new file mode 100644 index 0000000..b300ec3 --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserCountArgs.ts @@ -0,0 +1,5 @@ +import { UserWhereInput } from "./UserWhereInput"; + +export type UserCountArgs = { + where?: UserWhereInput; +}; diff --git a/apps/order-service-admin/src/api/user/UserCreateInput.ts b/apps/order-service-admin/src/api/user/UserCreateInput.ts new file mode 100644 index 0000000..e53c277 --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserCreateInput.ts @@ -0,0 +1,9 @@ +import { InputJsonValue } from "../../types"; + +export type UserCreateInput = { + firstName?: string | null; + lastName?: string | null; + password: string; + roles: InputJsonValue; + username: string; +}; diff --git a/apps/order-service-admin/src/api/user/UserFindManyArgs.ts b/apps/order-service-admin/src/api/user/UserFindManyArgs.ts new file mode 100644 index 0000000..b453f3e --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserFindManyArgs.ts @@ -0,0 +1,9 @@ +import { UserWhereInput } from "./UserWhereInput"; +import { UserOrderByInput } from "./UserOrderByInput"; + +export type UserFindManyArgs = { + where?: UserWhereInput; + orderBy?: Array; + skip?: number; + take?: number; +}; diff --git a/apps/order-service-admin/src/api/user/UserFindUniqueArgs.ts b/apps/order-service-admin/src/api/user/UserFindUniqueArgs.ts new file mode 100644 index 0000000..97d18e8 --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserFindUniqueArgs.ts @@ -0,0 +1,5 @@ +import { UserWhereUniqueInput } from "./UserWhereUniqueInput"; + +export type UserFindUniqueArgs = { + where: UserWhereUniqueInput; +}; diff --git a/apps/order-service-admin/src/api/user/UserListRelationFilter.ts b/apps/order-service-admin/src/api/user/UserListRelationFilter.ts new file mode 100644 index 0000000..4c4d06e --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserListRelationFilter.ts @@ -0,0 +1,7 @@ +import { UserWhereInput } from "./UserWhereInput"; + +export type UserListRelationFilter = { + every?: UserWhereInput; + some?: UserWhereInput; + none?: UserWhereInput; +}; diff --git a/apps/order-service-admin/src/api/user/UserOrderByInput.ts b/apps/order-service-admin/src/api/user/UserOrderByInput.ts new file mode 100644 index 0000000..36cee1e --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserOrderByInput.ts @@ -0,0 +1,12 @@ +import { SortOrder } from "../../util/SortOrder"; + +export type UserOrderByInput = { + createdAt?: SortOrder; + firstName?: SortOrder; + id?: SortOrder; + lastName?: SortOrder; + password?: SortOrder; + roles?: SortOrder; + updatedAt?: SortOrder; + username?: SortOrder; +}; diff --git a/apps/order-service-admin/src/api/user/UserUpdateInput.ts b/apps/order-service-admin/src/api/user/UserUpdateInput.ts new file mode 100644 index 0000000..f5ee977 --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserUpdateInput.ts @@ -0,0 +1,9 @@ +import { InputJsonValue } from "../../types"; + +export type UserUpdateInput = { + firstName?: string | null; + lastName?: string | null; + password?: string; + roles?: InputJsonValue; + username?: string; +}; diff --git a/apps/order-service-admin/src/api/user/UserWhereInput.ts b/apps/order-service-admin/src/api/user/UserWhereInput.ts new file mode 100644 index 0000000..22c10cb --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserWhereInput.ts @@ -0,0 +1,9 @@ +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { StringFilter } from "../../util/StringFilter"; + +export type UserWhereInput = { + firstName?: StringNullableFilter; + id?: StringFilter; + lastName?: StringNullableFilter; + username?: StringFilter; +}; diff --git a/apps/order-service-admin/src/api/user/UserWhereUniqueInput.ts b/apps/order-service-admin/src/api/user/UserWhereUniqueInput.ts new file mode 100644 index 0000000..309d343 --- /dev/null +++ b/apps/order-service-admin/src/api/user/UserWhereUniqueInput.ts @@ -0,0 +1,3 @@ +export type UserWhereUniqueInput = { + id: string; +}; diff --git a/apps/order-service-admin/src/auth-provider/ra-auth-http.ts b/apps/order-service-admin/src/auth-provider/ra-auth-http.ts new file mode 100644 index 0000000..c6eeba8 --- /dev/null +++ b/apps/order-service-admin/src/auth-provider/ra-auth-http.ts @@ -0,0 +1,78 @@ +import { gql } from "@apollo/client/core"; +import { AuthProvider } from "react-admin"; +import { + CREDENTIALS_LOCAL_STORAGE_ITEM, + USER_DATA_LOCAL_STORAGE_ITEM, +} from "../constants"; +import { Credentials, LoginMutateResult } from "../types"; +import { apolloClient } from "../data-provider/graphqlDataProvider"; + +const LOGIN = gql` + mutation login($username: String!, $password: String!) { + login(credentials: { username: $username, password: $password }) { + username + roles + } + } +`; + +export const httpAuthProvider: AuthProvider = { + login: async (credentials: Credentials) => { + const userData = await apolloClient.mutate({ + mutation: LOGIN, + variables: { + ...credentials, + }, + }); + + if (userData && userData.data?.login.username) { + localStorage.setItem( + CREDENTIALS_LOCAL_STORAGE_ITEM, + createBasicAuthorizationHeader( + credentials.username, + credentials.password + ) + ); + localStorage.setItem( + USER_DATA_LOCAL_STORAGE_ITEM, + JSON.stringify(userData.data) + ); + return Promise.resolve(); + } + return Promise.reject(); + }, + logout: () => { + localStorage.removeItem(CREDENTIALS_LOCAL_STORAGE_ITEM); + return Promise.resolve(); + }, + checkError: ({ status }: any) => { + if (status === 401 || status === 403) { + localStorage.removeItem(CREDENTIALS_LOCAL_STORAGE_ITEM); + return Promise.reject(); + } + return Promise.resolve(); + }, + checkAuth: () => { + return localStorage.getItem(CREDENTIALS_LOCAL_STORAGE_ITEM) + ? Promise.resolve() + : Promise.reject(); + }, + getPermissions: () => Promise.reject("Unknown method"), + getIdentity: () => { + const str = localStorage.getItem(USER_DATA_LOCAL_STORAGE_ITEM); + const userData: LoginMutateResult = JSON.parse(str || ""); + + return Promise.resolve({ + id: userData.login.username, + fullName: userData.login.username, + avatar: undefined, + }); + }, +}; + +function createBasicAuthorizationHeader( + username: string, + password: string +): string { + return `Basic ${btoa(`${username}:${password}`)}`; +} diff --git a/apps/order-service-admin/src/auth-provider/ra-auth-jwt.ts b/apps/order-service-admin/src/auth-provider/ra-auth-jwt.ts new file mode 100644 index 0000000..c8bcafc --- /dev/null +++ b/apps/order-service-admin/src/auth-provider/ra-auth-jwt.ts @@ -0,0 +1,72 @@ +import { gql } from "@apollo/client/core"; +import { AuthProvider } from "react-admin"; +import { + CREDENTIALS_LOCAL_STORAGE_ITEM, + USER_DATA_LOCAL_STORAGE_ITEM, +} from "../constants"; +import { Credentials, LoginMutateResult } from "../types"; +import { apolloClient } from "../data-provider/graphqlDataProvider"; + +const LOGIN = gql` + mutation login($username: String!, $password: String!) { + login(credentials: { username: $username, password: $password }) { + username + accessToken + } + } +`; + +export const jwtAuthProvider: AuthProvider = { + login: async (credentials: Credentials) => { + const userData = await apolloClient.mutate({ + mutation: LOGIN, + variables: { + ...credentials, + }, + }); + + if (userData && userData.data?.login.username) { + localStorage.setItem( + CREDENTIALS_LOCAL_STORAGE_ITEM, + createBearerAuthorizationHeader(userData.data.login?.accessToken) + ); + localStorage.setItem( + USER_DATA_LOCAL_STORAGE_ITEM, + JSON.stringify(userData.data) + ); + return Promise.resolve(); + } + return Promise.reject(); + }, + logout: () => { + localStorage.removeItem(CREDENTIALS_LOCAL_STORAGE_ITEM); + return Promise.resolve(); + }, + checkError: ({ status }: any) => { + if (status === 401 || status === 403) { + localStorage.removeItem(CREDENTIALS_LOCAL_STORAGE_ITEM); + return Promise.reject(); + } + return Promise.resolve(); + }, + checkAuth: () => { + return localStorage.getItem(CREDENTIALS_LOCAL_STORAGE_ITEM) + ? Promise.resolve() + : Promise.reject(); + }, + getPermissions: () => Promise.reject("Unknown method"), + getIdentity: () => { + const str = localStorage.getItem(USER_DATA_LOCAL_STORAGE_ITEM); + const userData: LoginMutateResult = JSON.parse(str || ""); + + return Promise.resolve({ + id: userData.login.username, + fullName: userData.login.username, + avatar: undefined, + }); + }, +}; + +export function createBearerAuthorizationHeader(accessToken: string) { + return `Bearer ${accessToken}`; +} diff --git a/apps/order-service-admin/src/auth.ts b/apps/order-service-admin/src/auth.ts new file mode 100644 index 0000000..498b026 --- /dev/null +++ b/apps/order-service-admin/src/auth.ts @@ -0,0 +1,34 @@ +import { EventEmitter } from "events"; +import { CREDENTIALS_LOCAL_STORAGE_ITEM } from "./constants"; +import { Credentials } from "./types"; + +const eventEmitter = new EventEmitter(); + +export function isAuthenticated(): boolean { + return Boolean(getCredentials()); +} + +export function listen(listener: (authenticated: boolean) => void): void { + eventEmitter.on("change", () => { + listener(isAuthenticated()); + }); +} + +export function setCredentials(credentials: Credentials) { + localStorage.setItem( + CREDENTIALS_LOCAL_STORAGE_ITEM, + JSON.stringify(credentials) + ); +} + +export function getCredentials(): Credentials | null { + const raw = localStorage.getItem(CREDENTIALS_LOCAL_STORAGE_ITEM); + if (raw === null) { + return null; + } + return JSON.parse(raw); +} + +export function removeCredentials(): void { + localStorage.removeItem(CREDENTIALS_LOCAL_STORAGE_ITEM); +} diff --git a/apps/order-service-admin/src/constants.ts b/apps/order-service-admin/src/constants.ts new file mode 100644 index 0000000..4b3ca4b --- /dev/null +++ b/apps/order-service-admin/src/constants.ts @@ -0,0 +1,2 @@ +export const CREDENTIALS_LOCAL_STORAGE_ITEM = "credentials"; +export const USER_DATA_LOCAL_STORAGE_ITEM = "userData"; diff --git a/apps/order-service-admin/src/customer/CustomerCreate.tsx b/apps/order-service-admin/src/customer/CustomerCreate.tsx new file mode 100644 index 0000000..fafb1a4 --- /dev/null +++ b/apps/order-service-admin/src/customer/CustomerCreate.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import { + Create, + SimpleForm, + CreateProps, + ReferenceInput, + SelectInput, + TextInput, +} from "react-admin"; +import { AddressTitle } from "../address/AddressTitle"; +import { OrderTitle } from "../order/OrderTitle"; + +export const CustomerCreate = (props: CreateProps): React.ReactElement => { + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/customer/CustomerEdit.tsx b/apps/order-service-admin/src/customer/CustomerEdit.tsx new file mode 100644 index 0000000..0698527 --- /dev/null +++ b/apps/order-service-admin/src/customer/CustomerEdit.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import { + Edit, + SimpleForm, + EditProps, + ReferenceInput, + SelectInput, + TextInput, +} from "react-admin"; +import { AddressTitle } from "../address/AddressTitle"; +import { OrderTitle } from "../order/OrderTitle"; + +export const CustomerEdit = (props: EditProps): React.ReactElement => { + return ( + + + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/customer/CustomerList.tsx b/apps/order-service-admin/src/customer/CustomerList.tsx new file mode 100644 index 0000000..21001b6 --- /dev/null +++ b/apps/order-service-admin/src/customer/CustomerList.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; +import { + List, + Datagrid, + ListProps, + ReferenceField, + TextField, + DateField, +} from "react-admin"; +import Pagination from "../Components/Pagination"; +import { ADDRESS_TITLE_FIELD } from "../address/AddressTitle"; +import { ORDER_TITLE_FIELD } from "../order/OrderTitle"; + +export const CustomerList = (props: ListProps): React.ReactElement => { + return ( + } + > + + + + + + + + + + + + + + {" "} + + + ); +}; diff --git a/apps/order-service-admin/src/customer/CustomerShow.tsx b/apps/order-service-admin/src/customer/CustomerShow.tsx new file mode 100644 index 0000000..bb1ec59 --- /dev/null +++ b/apps/order-service-admin/src/customer/CustomerShow.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { + Show, + SimpleShowLayout, + ShowProps, + ReferenceField, + TextField, + DateField, +} from "react-admin"; +import { ADDRESS_TITLE_FIELD } from "../address/AddressTitle"; +import { ORDER_TITLE_FIELD } from "../order/OrderTitle"; + +export const CustomerShow = (props: ShowProps): React.ReactElement => { + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/customer/CustomerTitle.ts b/apps/order-service-admin/src/customer/CustomerTitle.ts new file mode 100644 index 0000000..8122155 --- /dev/null +++ b/apps/order-service-admin/src/customer/CustomerTitle.ts @@ -0,0 +1,7 @@ +import { Customer as TCustomer } from "../api/customer/Customer"; + +export const CUSTOMER_TITLE_FIELD = "firstName"; + +export const CustomerTitle = (record: TCustomer): string => { + return record.firstName?.toString() || String(record.id); +}; diff --git a/apps/order-service-admin/src/data-provider/graphqlDataProvider.ts b/apps/order-service-admin/src/data-provider/graphqlDataProvider.ts new file mode 100644 index 0000000..2deee26 --- /dev/null +++ b/apps/order-service-admin/src/data-provider/graphqlDataProvider.ts @@ -0,0 +1,28 @@ +import buildGraphQLProvider from "ra-data-graphql-amplication"; +import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client"; +import { setContext } from "@apollo/client/link/context"; +import { CREDENTIALS_LOCAL_STORAGE_ITEM } from "../constants"; + +const httpLink = createHttpLink({ + uri: `${import.meta.env.VITE_REACT_APP_SERVER_URL}/graphql`, +}); + +// eslint-disable-next-line @typescript-eslint/naming-convention +const authLink = setContext((_, { headers }) => { + const token = localStorage.getItem(CREDENTIALS_LOCAL_STORAGE_ITEM); + return { + headers: { + ...headers, + authorization: token ? token : "", + }, + }; +}); + +export const apolloClient = new ApolloClient({ + cache: new InMemoryCache(), + link: authLink.concat(httpLink), +}); + +export default buildGraphQLProvider({ + client: apolloClient, +}); diff --git a/apps/order-service-admin/src/index.css b/apps/order-service-admin/src/index.css new file mode 100644 index 0000000..8686848 --- /dev/null +++ b/apps/order-service-admin/src/index.css @@ -0,0 +1,26 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#root { + height: 100vh; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.amp-breadcrumbs { + padding: var(--default-spacing); +} + +.entity-id { + color: var(--primary); + text-decoration: underline; +} diff --git a/apps/order-service-admin/src/index.tsx b/apps/order-service-admin/src/index.tsx new file mode 100644 index 0000000..7467839 --- /dev/null +++ b/apps/order-service-admin/src/index.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); diff --git a/apps/order-service-admin/src/login.scss b/apps/order-service-admin/src/login.scss new file mode 100644 index 0000000..667d8d2 --- /dev/null +++ b/apps/order-service-admin/src/login.scss @@ -0,0 +1,119 @@ +:root { + --surface: #15192c; /*dark: black100 */ + --white: #15192c; /*dark: black100 */ + + --black100: #ffffff; /*dark: white */ + --black90: #b7bac7; /*dark: black10 */ + --black80: #a3a8b8; /*dark: black20 */ + --black60: #80869d; /*dark: black30 */ + --black40: #686f8c; /*dark: black40 */ + --black30: #515873; /*dark: black50 */ + --black20: #444b66; /*dark: black60 */ + --black10: #373d57; /*dark: black70 */ + --black5: #2c3249; /*dark: black80 */ + --black2: #22273c; /*dark: black90 */ + + --primary: #7950ed; +} + +.login-page { + height: 100vh; + width: 100%; + background-color: var(--surface); + color: var(--black100); + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + &__wrapper { + display: flex; + align-items: stretch; + justify-content: center; + flex-direction: row; + } + + &__box { + text-align: center; + width: 340px; + background-color: var(--black2); + border-radius: var(--small-border-radius); + margin: 1rem; + padding: 1rem; + border: 1px solid var(--black10); + display: flex; + flex-direction: column; + align-items: center; + justify-content: stretch; + + h2 { + font-size: 18px; + } + img { + width: 48px; + } + + &__message { + color: var(--black80); + font-size: 14px; + line-height: 22px; + } + + button, + .MuiButton-contained { + box-sizing: border-box; + background-color: var(--primary); + width: 300px; + margin-top: 0.5rem; + margin-bottom: 1rem; + margin-top: auto; + &:hover, + &:active, + &:focus { + background-color: var(--primary); + } + } + } + + form { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + margin-top: 2rem; + + label { + span { + display: block; + text-align: left; + font-size: 12px; + color: var(--black60); + } + } + + input { + box-sizing: border-box; + background-color: var(--white); + border: 1px solid var(--black10); + padding: 0.5rem; + margin-bottom: 1rem; + outline: none; + border-radius: var(--small-border-radius); + width: 300px; + color: var(--black100); + &:hover, + &:active, + &:focus { + border: 1px solid var(--black100); + } + } + } + + &__read-more { + color: var(--black80); + a { + color: var(--black100); + text-decoration: none; + } + } +} diff --git a/apps/order-service-admin/src/order/OrderCreate.tsx b/apps/order-service-admin/src/order/OrderCreate.tsx new file mode 100644 index 0000000..3e73a43 --- /dev/null +++ b/apps/order-service-admin/src/order/OrderCreate.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { + Create, + SimpleForm, + CreateProps, + ReferenceInput, + SelectInput, + NumberInput, +} from "react-admin"; +import { CustomerTitle } from "../customer/CustomerTitle"; +import { ProductTitle } from "../product/ProductTitle"; + +export const OrderCreate = (props: CreateProps): React.ReactElement => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/order/OrderEdit.tsx b/apps/order-service-admin/src/order/OrderEdit.tsx new file mode 100644 index 0000000..71a2608 --- /dev/null +++ b/apps/order-service-admin/src/order/OrderEdit.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { + Edit, + SimpleForm, + EditProps, + ReferenceInput, + SelectInput, + NumberInput, +} from "react-admin"; +import { CustomerTitle } from "../customer/CustomerTitle"; +import { ProductTitle } from "../product/ProductTitle"; + +export const OrderEdit = (props: EditProps): React.ReactElement => { + return ( + + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/order/OrderList.tsx b/apps/order-service-admin/src/order/OrderList.tsx new file mode 100644 index 0000000..cb4cec7 --- /dev/null +++ b/apps/order-service-admin/src/order/OrderList.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import { + List, + Datagrid, + ListProps, + DateField, + ReferenceField, + TextField, +} from "react-admin"; +import Pagination from "../Components/Pagination"; +import { CUSTOMER_TITLE_FIELD } from "../customer/CustomerTitle"; +import { PRODUCT_TITLE_FIELD } from "../product/ProductTitle"; + +export const OrderList = (props: ListProps): React.ReactElement => { + return ( + }> + + + + + + + + + + + + + {" "} + + + ); +}; diff --git a/apps/order-service-admin/src/order/OrderShow.tsx b/apps/order-service-admin/src/order/OrderShow.tsx new file mode 100644 index 0000000..da3429f --- /dev/null +++ b/apps/order-service-admin/src/order/OrderShow.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; +import { + Show, + SimpleShowLayout, + ShowProps, + DateField, + ReferenceField, + TextField, +} from "react-admin"; +import { CUSTOMER_TITLE_FIELD } from "../customer/CustomerTitle"; +import { PRODUCT_TITLE_FIELD } from "../product/ProductTitle"; + +export const OrderShow = (props: ShowProps): React.ReactElement => { + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/order-service-admin/src/order/OrderTitle.ts b/apps/order-service-admin/src/order/OrderTitle.ts new file mode 100644 index 0000000..18af368 --- /dev/null +++ b/apps/order-service-admin/src/order/OrderTitle.ts @@ -0,0 +1,7 @@ +import { Order as TOrder } from "../api/order/Order"; + +export const ORDER_TITLE_FIELD = "id"; + +export const OrderTitle = (record: TOrder): string => { + return record.id?.toString() || String(record.id); +}; diff --git a/apps/order-service-admin/src/pages/Dashboard.tsx b/apps/order-service-admin/src/pages/Dashboard.tsx new file mode 100644 index 0000000..29776bc --- /dev/null +++ b/apps/order-service-admin/src/pages/Dashboard.tsx @@ -0,0 +1,12 @@ +import * as React from "react"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import { Title } from "react-admin"; +const Dashboard = () => ( + + + <CardContent>Welcome</CardContent> + </Card> +); + +export default Dashboard; diff --git a/apps/order-service-admin/src/product/ProductCreate.tsx b/apps/order-service-admin/src/product/ProductCreate.tsx new file mode 100644 index 0000000..ef3fe9a --- /dev/null +++ b/apps/order-service-admin/src/product/ProductCreate.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; + +import { + Create, + SimpleForm, + CreateProps, + TextInput, + NumberInput, + ReferenceInput, + SelectInput, +} from "react-admin"; + +import { OrderTitle } from "../order/OrderTitle"; + +export const ProductCreate = (props: CreateProps): React.ReactElement => { + return ( + <Create {...props}> + <SimpleForm> + <TextInput label="Description" multiline source="description" /> + <NumberInput label="Item Price" source="itemPrice" /> + <TextInput label="Name" source="name" /> + <ReferenceInput source="orders.id" reference="Order" label="Orders"> + <SelectInput optionText={OrderTitle} /> + </ReferenceInput> + </SimpleForm> + </Create> + ); +}; diff --git a/apps/order-service-admin/src/product/ProductEdit.tsx b/apps/order-service-admin/src/product/ProductEdit.tsx new file mode 100644 index 0000000..28099c1 --- /dev/null +++ b/apps/order-service-admin/src/product/ProductEdit.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; + +import { + Edit, + SimpleForm, + EditProps, + TextInput, + NumberInput, + ReferenceInput, + SelectInput, +} from "react-admin"; + +import { OrderTitle } from "../order/OrderTitle"; + +export const ProductEdit = (props: EditProps): React.ReactElement => { + return ( + <Edit {...props}> + <SimpleForm> + <TextInput label="Description" multiline source="description" /> + <NumberInput label="Item Price" source="itemPrice" /> + <TextInput label="Name" source="name" /> + <ReferenceInput source="orders.id" reference="Order" label="Orders"> + <SelectInput optionText={OrderTitle} /> + </ReferenceInput> + </SimpleForm> + </Edit> + ); +}; diff --git a/apps/order-service-admin/src/product/ProductList.tsx b/apps/order-service-admin/src/product/ProductList.tsx new file mode 100644 index 0000000..3a2ca23 --- /dev/null +++ b/apps/order-service-admin/src/product/ProductList.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { + List, + Datagrid, + ListProps, + DateField, + TextField, + ReferenceField, +} from "react-admin"; +import Pagination from "../Components/Pagination"; +import { ORDER_TITLE_FIELD } from "../order/OrderTitle"; + +export const ProductList = (props: ListProps): React.ReactElement => { + return ( + <List + {...props} + title={"Products"} + perPage={50} + pagination={<Pagination />} + > + <Datagrid rowClick="show" bulkActionButtons={false}> + <DateField source="createdAt" label="Created At" /> + <TextField label="Description" source="description" /> + <TextField label="ID" source="id" /> + <TextField label="Item Price" source="itemPrice" /> + <TextField label="Name" source="name" /> + <ReferenceField label="Orders" source="order.id" reference="Order"> + <TextField source={ORDER_TITLE_FIELD} /> + </ReferenceField> + <DateField source="updatedAt" label="Updated At" />{" "} + </Datagrid> + </List> + ); +}; diff --git a/apps/order-service-admin/src/product/ProductShow.tsx b/apps/order-service-admin/src/product/ProductShow.tsx new file mode 100644 index 0000000..f262ef3 --- /dev/null +++ b/apps/order-service-admin/src/product/ProductShow.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { + Show, + SimpleShowLayout, + ShowProps, + DateField, + TextField, + ReferenceField, +} from "react-admin"; +import { ORDER_TITLE_FIELD } from "../order/OrderTitle"; + +export const ProductShow = (props: ShowProps): React.ReactElement => { + return ( + <Show {...props}> + <SimpleShowLayout> + <DateField source="createdAt" label="Created At" /> + <TextField label="Description" source="description" /> + <TextField label="ID" source="id" /> + <TextField label="Item Price" source="itemPrice" /> + <TextField label="Name" source="name" /> + <ReferenceField label="Orders" source="order.id" reference="Order"> + <TextField source={ORDER_TITLE_FIELD} /> + </ReferenceField> + <DateField source="updatedAt" label="Updated At" /> + </SimpleShowLayout> + </Show> + ); +}; diff --git a/apps/order-service-admin/src/product/ProductTitle.ts b/apps/order-service-admin/src/product/ProductTitle.ts new file mode 100644 index 0000000..78e9c2c --- /dev/null +++ b/apps/order-service-admin/src/product/ProductTitle.ts @@ -0,0 +1,7 @@ +import { Product as TProduct } from "../api/product/Product"; + +export const PRODUCT_TITLE_FIELD = "name"; + +export const ProductTitle = (record: TProduct): string => { + return record.name?.toString() || String(record.id); +}; diff --git a/apps/order-service-admin/src/reportWebVitals.ts b/apps/order-service-admin/src/reportWebVitals.ts new file mode 100644 index 0000000..821a6cd --- /dev/null +++ b/apps/order-service-admin/src/reportWebVitals.ts @@ -0,0 +1,17 @@ +import { ReportHandler } from "web-vitals"; + +const reportWebVitals = (onPerfEntry?: ReportHandler): void => { + if (onPerfEntry && onPerfEntry instanceof Function) { + void import("web-vitals").then( + ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + } + ); + } +}; + +export default reportWebVitals; diff --git a/apps/order-service-admin/src/setupTests.ts b/apps/order-service-admin/src/setupTests.ts new file mode 100644 index 0000000..1dd407a --- /dev/null +++ b/apps/order-service-admin/src/setupTests.ts @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import "@testing-library/jest-dom"; diff --git a/apps/order-service-admin/src/theme/theme.ts b/apps/order-service-admin/src/theme/theme.ts new file mode 100644 index 0000000..bcfca0e --- /dev/null +++ b/apps/order-service-admin/src/theme/theme.ts @@ -0,0 +1,33 @@ +import { defaultTheme } from "react-admin"; +import { createTheme, ThemeOptions } from "@mui/material/styles"; +import { merge } from "lodash"; +import createPalette from "@mui/material/styles/createPalette"; + +const palette = createPalette( + merge({}, defaultTheme.palette, { + primary: { + main: "#20a4f3", + }, + secondary: { + main: "#7950ed", + }, + error: { + main: "#e93c51", + }, + warning: { + main: "#f6aa50", + }, + info: { + main: "#144bc1", + }, + success: { + main: "#31c587", + }, + }) +); + +const themeOptions: ThemeOptions = { + palette, +}; + +export const theme = createTheme(merge({}, defaultTheme, themeOptions)); diff --git a/apps/order-service-admin/src/types.ts b/apps/order-service-admin/src/types.ts new file mode 100644 index 0000000..45a457d --- /dev/null +++ b/apps/order-service-admin/src/types.ts @@ -0,0 +1,13 @@ +import { JsonValue } from "type-fest"; + +export type Credentials = { + username: string; + password: string; +}; +export type LoginMutateResult = { + login: { + username: string; + accessToken: string; + }; +}; +export type InputJsonValue = Omit<JsonValue, "null">; diff --git a/apps/order-service-admin/src/user/EnumRoles.ts b/apps/order-service-admin/src/user/EnumRoles.ts new file mode 100644 index 0000000..3df7048 --- /dev/null +++ b/apps/order-service-admin/src/user/EnumRoles.ts @@ -0,0 +1,3 @@ +export enum EnumRoles { + User = "user", +} diff --git a/apps/order-service-admin/src/user/RolesOptions.ts b/apps/order-service-admin/src/user/RolesOptions.ts new file mode 100644 index 0000000..5e30fe9 --- /dev/null +++ b/apps/order-service-admin/src/user/RolesOptions.ts @@ -0,0 +1,11 @@ +import { ROLES } from "./roles"; + +declare interface Role { + name: string; + displayName: string; +} + +export const ROLES_OPTIONS = ROLES.map((role: Role) => ({ + value: role.name, + label: role.displayName, +})); diff --git a/apps/order-service-admin/src/user/UserCreate.tsx b/apps/order-service-admin/src/user/UserCreate.tsx new file mode 100644 index 0000000..def7787 --- /dev/null +++ b/apps/order-service-admin/src/user/UserCreate.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; + +import { + Create, + SimpleForm, + CreateProps, + TextInput, + PasswordInput, + SelectArrayInput, +} from "react-admin"; + +import { ROLES_OPTIONS } from "../user/RolesOptions"; + +export const UserCreate = (props: CreateProps): React.ReactElement => { + return ( + <Create {...props}> + <SimpleForm> + <TextInput label="First Name" source="firstName" /> + <TextInput label="Last Name" source="lastName" /> + <PasswordInput label="Password" source="password" /> + <SelectArrayInput + source="roles" + choices={ROLES_OPTIONS} + optionText="label" + optionValue="value" + /> + <TextInput label="Username" source="username" /> + </SimpleForm> + </Create> + ); +}; diff --git a/apps/order-service-admin/src/user/UserEdit.tsx b/apps/order-service-admin/src/user/UserEdit.tsx new file mode 100644 index 0000000..8565bf9 --- /dev/null +++ b/apps/order-service-admin/src/user/UserEdit.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { + Edit, + SimpleForm, + EditProps, + TextInput, + PasswordInput, + SelectArrayInput, +} from "react-admin"; +import { ROLES_OPTIONS } from "../user/RolesOptions"; + +export const UserEdit = (props: EditProps): React.ReactElement => { + return ( + <Edit {...props}> + <SimpleForm> + <TextInput label="First Name" source="firstName" /> + <TextInput label="Last Name" source="lastName" /> + <PasswordInput label="Password" source="password" /> + <SelectArrayInput + source="roles" + choices={ROLES_OPTIONS} + optionText="label" + optionValue="value" + /> + <TextInput label="Username" source="username" /> + </SimpleForm> + </Edit> + ); +}; diff --git a/apps/order-service-admin/src/user/UserList.tsx b/apps/order-service-admin/src/user/UserList.tsx new file mode 100644 index 0000000..c5adf63 --- /dev/null +++ b/apps/order-service-admin/src/user/UserList.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { List, Datagrid, ListProps, DateField, TextField } from "react-admin"; +import Pagination from "../Components/Pagination"; + +export const UserList = (props: ListProps): React.ReactElement => { + return ( + <List {...props} title={"Users"} perPage={50} pagination={<Pagination />}> + <Datagrid rowClick="show" bulkActionButtons={false}> + <DateField source="createdAt" label="Created At" /> + <TextField label="First Name" source="firstName" /> + <TextField label="ID" source="id" /> + <TextField label="Last Name" source="lastName" /> + <TextField label="Roles" source="roles" /> + <DateField source="updatedAt" label="Updated At" /> + <TextField label="Username" source="username" />{" "} + </Datagrid> + </List> + ); +}; diff --git a/apps/order-service-admin/src/user/UserShow.tsx b/apps/order-service-admin/src/user/UserShow.tsx new file mode 100644 index 0000000..dc33a5b --- /dev/null +++ b/apps/order-service-admin/src/user/UserShow.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { + Show, + SimpleShowLayout, + ShowProps, + DateField, + TextField, +} from "react-admin"; + +export const UserShow = (props: ShowProps): React.ReactElement => { + return ( + <Show {...props}> + <SimpleShowLayout> + <DateField source="createdAt" label="Created At" /> + <TextField label="First Name" source="firstName" /> + <TextField label="ID" source="id" /> + <TextField label="Last Name" source="lastName" /> + <TextField label="Roles" source="roles" /> + <DateField source="updatedAt" label="Updated At" /> + <TextField label="Username" source="username" /> + </SimpleShowLayout> + </Show> + ); +}; diff --git a/apps/order-service-admin/src/user/UserTitle.ts b/apps/order-service-admin/src/user/UserTitle.ts new file mode 100644 index 0000000..336f609 --- /dev/null +++ b/apps/order-service-admin/src/user/UserTitle.ts @@ -0,0 +1,7 @@ +import { User as TUser } from "../api/user/User"; + +export const USER_TITLE_FIELD = "firstName"; + +export const UserTitle = (record: TUser): string => { + return record.firstName?.toString() || String(record.id); +}; diff --git a/apps/order-service-admin/src/user/roles.ts b/apps/order-service-admin/src/user/roles.ts new file mode 100644 index 0000000..732870a --- /dev/null +++ b/apps/order-service-admin/src/user/roles.ts @@ -0,0 +1,6 @@ +export const ROLES = [ + { + name: "user", + displayName: "User", + }, +]; diff --git a/apps/order-service-admin/src/util/BooleanFilter.ts b/apps/order-service-admin/src/util/BooleanFilter.ts new file mode 100644 index 0000000..a142d58 --- /dev/null +++ b/apps/order-service-admin/src/util/BooleanFilter.ts @@ -0,0 +1,4 @@ +export class BooleanFilter { + equals?: boolean; + not?: boolean; +} diff --git a/apps/order-service-admin/src/util/BooleanNullableFilter.ts b/apps/order-service-admin/src/util/BooleanNullableFilter.ts new file mode 100644 index 0000000..b94aefc --- /dev/null +++ b/apps/order-service-admin/src/util/BooleanNullableFilter.ts @@ -0,0 +1,4 @@ +export class BooleanNullableFilter { + equals?: boolean | null; + not?: boolean | null; +} diff --git a/apps/order-service-admin/src/util/DateTimeFilter.ts b/apps/order-service-admin/src/util/DateTimeFilter.ts new file mode 100644 index 0000000..cd8d213 --- /dev/null +++ b/apps/order-service-admin/src/util/DateTimeFilter.ts @@ -0,0 +1,10 @@ +export class DateTimeFilter { + equals?: Date; + not?: Date; + in?: Date[]; + notIn?: Date[]; + lt?: Date; + lte?: Date; + gt?: Date; + gte?: Date; +} diff --git a/apps/order-service-admin/src/util/DateTimeNullableFilter.ts b/apps/order-service-admin/src/util/DateTimeNullableFilter.ts new file mode 100644 index 0000000..2f9c7b3 --- /dev/null +++ b/apps/order-service-admin/src/util/DateTimeNullableFilter.ts @@ -0,0 +1,10 @@ +export class DateTimeNullableFilter { + equals?: Date | null; + in?: Date[] | null; + notIn?: Date[] | null; + lt?: Date; + lte?: Date; + gt?: Date; + gte?: Date; + not?: Date; +} diff --git a/apps/order-service-admin/src/util/FloatFilter.ts b/apps/order-service-admin/src/util/FloatFilter.ts new file mode 100644 index 0000000..62aeb14 --- /dev/null +++ b/apps/order-service-admin/src/util/FloatFilter.ts @@ -0,0 +1,10 @@ +export class FloatFilter { + equals?: number; + in?: number[]; + notIn?: number[]; + lt?: number; + lte?: number; + gt?: number; + gte?: number; + not?: number; +} diff --git a/apps/order-service-admin/src/util/FloatNullableFilter.ts b/apps/order-service-admin/src/util/FloatNullableFilter.ts new file mode 100644 index 0000000..d7bb163 --- /dev/null +++ b/apps/order-service-admin/src/util/FloatNullableFilter.ts @@ -0,0 +1,10 @@ +export class FloatNullableFilter { + equals?: number | null; + in?: number[] | null; + notIn?: number[] | null; + lt?: number; + lte?: number; + gt?: number; + gte?: number; + not?: number; +} diff --git a/apps/order-service-admin/src/util/IntFilter.ts b/apps/order-service-admin/src/util/IntFilter.ts new file mode 100644 index 0000000..3dc0221 --- /dev/null +++ b/apps/order-service-admin/src/util/IntFilter.ts @@ -0,0 +1,10 @@ +export class IntFilter { + equals?: number; + in?: number[]; + notIn?: number[]; + lt?: number; + lte?: number; + gt?: number; + gte?: number; + not?: number; +} diff --git a/apps/order-service-admin/src/util/IntNullableFilter.ts b/apps/order-service-admin/src/util/IntNullableFilter.ts new file mode 100644 index 0000000..2107cae --- /dev/null +++ b/apps/order-service-admin/src/util/IntNullableFilter.ts @@ -0,0 +1,10 @@ +export class IntNullableFilter { + equals?: number | null; + in?: number[] | null; + notIn?: number[] | null; + lt?: number; + lte?: number; + gt?: number; + gte?: number; + not?: number; +} diff --git a/apps/order-service-admin/src/util/JsonFilter.ts b/apps/order-service-admin/src/util/JsonFilter.ts new file mode 100644 index 0000000..cc44763 --- /dev/null +++ b/apps/order-service-admin/src/util/JsonFilter.ts @@ -0,0 +1,5 @@ +import { InputJsonValue } from "../types"; +export class JsonFilter { + equals?: InputJsonValue; + not?: InputJsonValue; +} diff --git a/apps/order-service-admin/src/util/JsonNullableFilter.ts b/apps/order-service-admin/src/util/JsonNullableFilter.ts new file mode 100644 index 0000000..e6d1506 --- /dev/null +++ b/apps/order-service-admin/src/util/JsonNullableFilter.ts @@ -0,0 +1,5 @@ +import { JsonValue } from "type-fest"; +export class JsonNullableFilter { + equals?: JsonValue | null; + not?: JsonValue | null; +} diff --git a/apps/order-service-admin/src/util/MetaQueryPayload.ts b/apps/order-service-admin/src/util/MetaQueryPayload.ts new file mode 100644 index 0000000..bc3175b --- /dev/null +++ b/apps/order-service-admin/src/util/MetaQueryPayload.ts @@ -0,0 +1,3 @@ +export class MetaQueryPayload { + count!: number; +} diff --git a/apps/order-service-admin/src/util/QueryMode.ts b/apps/order-service-admin/src/util/QueryMode.ts new file mode 100644 index 0000000..8a2164e --- /dev/null +++ b/apps/order-service-admin/src/util/QueryMode.ts @@ -0,0 +1,4 @@ +export enum QueryMode { + Default = "default", + Insensitive = "insensitive", +} diff --git a/apps/order-service-admin/src/util/SortOrder.ts b/apps/order-service-admin/src/util/SortOrder.ts new file mode 100644 index 0000000..a5bcdb6 --- /dev/null +++ b/apps/order-service-admin/src/util/SortOrder.ts @@ -0,0 +1,4 @@ +export enum SortOrder { + Asc = "asc", + Desc = "desc", +} diff --git a/apps/order-service-admin/src/util/StringFilter.ts b/apps/order-service-admin/src/util/StringFilter.ts new file mode 100644 index 0000000..c2e26c5 --- /dev/null +++ b/apps/order-service-admin/src/util/StringFilter.ts @@ -0,0 +1,16 @@ +import { QueryMode } from "./QueryMode"; + +export class StringFilter { + equals?: string; + in?: string[]; + notIn?: string[]; + lt?: string; + lte?: string; + gt?: string; + gte?: string; + contains?: string; + startsWith?: string; + endsWith?: string; + mode?: QueryMode; + not?: string; +} diff --git a/apps/order-service-admin/src/util/StringNullableFilter.ts b/apps/order-service-admin/src/util/StringNullableFilter.ts new file mode 100644 index 0000000..e1e37ec --- /dev/null +++ b/apps/order-service-admin/src/util/StringNullableFilter.ts @@ -0,0 +1,15 @@ +import { QueryMode } from "./QueryMode"; +export class StringNullableFilter { + equals?: string | null; + in?: string[] | null; + notIn?: string[] | null; + lt?: string; + lte?: string; + gt?: string; + gte?: string; + contains?: string; + startsWith?: string; + endsWith?: string; + mode?: QueryMode; + not?: string; +} diff --git a/apps/order-service-admin/src/vite-env.d.ts b/apps/order-service-admin/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/order-service-admin/src/vite-env.d.ts @@ -0,0 +1 @@ +/// <reference types="vite/client" /> diff --git a/apps/order-service-admin/tsconfig.json b/apps/order-service-admin/tsconfig.json new file mode 100644 index 0000000..31cc780 --- /dev/null +++ b/apps/order-service-admin/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true + }, + "include": ["src"], + "exclude": ["./node_modules"] +} diff --git a/apps/order-service-admin/vite.config.ts b/apps/order-service-admin/vite.config.ts new file mode 100644 index 0000000..cf64d08 --- /dev/null +++ b/apps/order-service-admin/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + define: { + "process.env": process.env, + }, + server: { + host: true, + }, + base: "./", +}); diff --git a/apps/order-service/.dockerignore b/apps/order-service/.dockerignore new file mode 100644 index 0000000..cb5c30b --- /dev/null +++ b/apps/order-service/.dockerignore @@ -0,0 +1,8 @@ +.dockerignore +docker-compose.yml +Dockerfile +dist/ +node_modules +.env +.gitignore +.prettierignore \ No newline at end of file diff --git a/apps/order-service/.env b/apps/order-service/.env new file mode 100644 index 0000000..e2034b5 --- /dev/null +++ b/apps/order-service/.env @@ -0,0 +1,14 @@ +BCRYPT_SALT=10 +COMPOSE_PROJECT_NAME=amp_clk6vqvbu02nmk5016yp4sqvv +DB_NAME=my-db +DB_PASSWORD=admin +DB_PORT=5432 +DB_URL=postgres://admin:admin@localhost:5432/my-db +DB_USER=admin +JWT_EXPIRATION=2d +JWT_SECRET_KEY=Change_ME!!! +KAFKA_BROKERS=localhost:9092 +KAFKA_CLIENT_ID=order-service +KAFKA_ENABLE_SSL=false +KAFKA_GROUP_ID=order-service +PORT=3000 \ No newline at end of file diff --git a/apps/order-service/.gitignore b/apps/order-service/.gitignore new file mode 100644 index 0000000..08c9980 --- /dev/null +++ b/apps/order-service/.gitignore @@ -0,0 +1,5 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +/node_modules +/dist +.DS_Store diff --git a/apps/order-service/.prettierignore b/apps/order-service/.prettierignore new file mode 100644 index 0000000..e48f355 --- /dev/null +++ b/apps/order-service/.prettierignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +prisma/migrations/ +package-lock.json +coverage/ \ No newline at end of file diff --git a/apps/order-service/Dockerfile b/apps/order-service/Dockerfile new file mode 100644 index 0000000..80dd8d3 --- /dev/null +++ b/apps/order-service/Dockerfile @@ -0,0 +1,68 @@ +# multi-stage: base (build) +FROM node:18.13.0 AS base + +# create directory where the application will be built +WORKDIR /app + +# copy over the dependency manifests, both the package.json +# and the package-lock.json are copied over +COPY package*.json ./ + +# installs packages and their dependencies +RUN npm install + +# copy over the prisma schema +COPY prisma/schema.prisma ./prisma/ + +# generate the prisma client based on the schema +RUN npm run prisma:generate + +# copy over the code base +COPY . . + +# create the bundle of the application +RUN npm run build + +# multi-stage: production (runtime) +FROM node:18.13.0-slim AS production + +# create arguments of builds time variables +ARG user=amplication +ARG group=${user} +ARG uid=1001 +ARG gid=$uid + +# [temporary] work around to be able to run prisma +RUN apt-get update -y && apt-get install -y openssl + +# create directory where the application will be executed from +WORKDIR /app + +# add the user and group +RUN groupadd --gid ${gid} ${user} +RUN useradd --uid ${uid} --gid ${gid} -m ${user} + +# copy over the bundled code from the build stage +COPY --from=base /app/node_modules/ ./node_modules +COPY --from=base /app/package.json ./package.json +COPY --from=base /app/dist ./dist +COPY --from=base /app/prisma ./prisma +COPY --from=base /app/scripts ./scripts +COPY --from=base /app/src ./src +COPY --from=base /app/tsconfig* ./ + +# change ownership of the workspace directory +RUN chown -R ${uid}:${gid} /app/ + +# get rid of the development dependencies +RUN npm install --production + +# set user to the created non-privileged user +USER ${user} + +# expose a specific port on the docker container +ENV PORT=3000 +EXPOSE ${PORT} + +# start the server using the previously build application +CMD [ "node", "./dist/main.js" ] diff --git a/apps/order-service/README.md b/apps/order-service/README.md new file mode 100644 index 0000000..bca0445 --- /dev/null +++ b/apps/order-service/README.md @@ -0,0 +1,64 @@ +<p align="right"> + <a href="https://amplication.com" target="_blank"> + <img alt="amplication-logo" height="70" alt="Amplication Logo" src="https://amplication.com/images/logo.svg"/> + </a> +</p> + +# Introduction + +This service was generated with Amplication. The server-side of the generated project. This component provides the different backend services - i.e., REST API, GraphQL API, authentication, authorization, logging, data validation and the connection to the database. Additional information about the server component and the architecture around it, can be found on the [documentation](https://docs.amplication.com/guides/getting-started) site. + +# Getting started + +## Step 1: Configuration + +Configuration for the server component can be provided through the use of environment variables. These can be passed to the application via the use of the `.env` file in the base directory of the generated service. Below a table can be found which show the different variables that can be passed - these are the variables which exist by default, through the use of plugins additional integrations could require additional values. These values are provided default values after generation, change them to the desired values. + +| Variable | Description | Value | +| -------------------- | -------------------------------------------- | ------------------------------------------------------------------- | +| BCRYPT_SALT | the string used for hashing | [random-string] | +| COMPOSE_PROJECT_NAME | the identifier of the service plus prefix | amp_[service-identifier] | +| PORT | the port on which to run the server | 3000 | +| DB_URL | the connection url for the database | [db-provider]://[username]:[password]@localhost:[db-port]/[db-name] | +| DB_PORT | the port used by the database instance | [db-provider-port] | +| DB_USER | the username used to connect to the database | [username] | +| DB_PASSWORD | the password used to connect to the database | [password] | +| DB_NAME | the name of the database | [service-name] / [project-name] | +| JWT_SECRET_KEY | the secret used to sign the json-web token | [secret] | +| JWT_EXPIRATION | the expiration time for the json-web token | 2d | + +> **Note** +> Amplication generates default values and stores them under the .env file. It is advised to use some form of secrets manager/vault solution when using in production. + +## Step 2.1: Scripts - pre-requisites + +After configuration of the server the next step would be to run the application. Before running the server side of the component, make sure that the different pre-requisites are met - i.e., node.js [^16.x], npm, docker. After the setup of the pre-requisites the server component can be started. + +```sh +# installation of the dependencies +$ npm install + +# generate the prisma client +$ npm run prisma:generate +``` + +## Step 2.2: Scripts - local development + +```sh +# start the database where the server component will connect to +$ npm run docker:dev + +# initialize the database +$ npm run db:init + +# start the server component +$ npm run start +``` +By default, your app comes with one user with the username "admin" and password "admin". + +## Step 2.2: Scripts - container based development + +```shell +# start the server component as a docker container +$ npm run compose:up +``` diff --git a/apps/order-service/docker-compose.dev.yml b/apps/order-service/docker-compose.dev.yml new file mode 100644 index 0000000..ef68f7e --- /dev/null +++ b/apps/order-service/docker-compose.dev.yml @@ -0,0 +1,49 @@ +version: "3" +services: + db: + image: postgres:12 + ports: + - ${DB_PORT}:5432 + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres:/var/lib/postgresql/data + zookeeper: + image: confluentinc/cp-zookeeper:5.2.4 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - 2181:2181 + kafka: + image: confluentinc/cp-kafka:7.3.1 + depends_on: + - zookeeper + ports: + - 9092:9092 + - 9997:9997 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - zookeeper + - kafka + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 + KAFKA_CLUSTERS_0_JMXPORT: 9997 +volumes: + postgres: ~ diff --git a/apps/order-service/docker-compose.yml b/apps/order-service/docker-compose.yml new file mode 100644 index 0000000..969efed --- /dev/null +++ b/apps/order-service/docker-compose.yml @@ -0,0 +1,89 @@ +version: "3" +services: + server: + build: + context: . + args: + NPM_LOG_LEVEL: notice + ports: + - ${PORT}:3000 + environment: + BCRYPT_SALT: ${BCRYPT_SALT} + DB_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME} + JWT_SECRET_KEY: ${JWT_SECRET_KEY} + JWT_EXPIRATION: ${JWT_EXPIRATION} + KAFKA_BROKERS: kafka:9092 + KAFKA_ENABLE_SSL: ${KAFKA_ENABLE_SSL} + KAFKA_CLIENT_ID: ${KAFKA_CLIENT_ID} + KAFKA_GROUP_ID: ${KAFKA_GROUP_ID} + depends_on: + - migrate + restart: on-failure + migrate: + build: + context: . + args: + NPM_LOG_LEVEL: notice + command: npm run db:init + working_dir: /app/server + environment: + BCRYPT_SALT: ${BCRYPT_SALT} + DB_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME} + depends_on: + db: + condition: service_healthy + db: + image: postgres:12 + ports: + - ${DB_PORT}:5432 + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + volumes: + - postgres:/var/lib/postgresql/data + healthcheck: + test: + - CMD-SHELL + - pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER} + timeout: 45s + interval: 10s + retries: 10 + zookeeper: + image: confluentinc/cp-zookeeper:5.2.4 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - 2181:2181 + kafka: + image: confluentinc/cp-kafka:7.3.1 + depends_on: + - zookeeper + ports: + - 9092:9092 + - 9997:9997 + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://kafka:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + kafka-ui: + container_name: kafka-ui + image: provectuslabs/kafka-ui:latest + ports: + - 8080:8080 + depends_on: + - zookeeper + - kafka + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 + KAFKA_CLUSTERS_0_JMXPORT: 9997 +volumes: + postgres: ~ diff --git a/apps/order-service/nest-cli.json b/apps/order-service/nest-cli.json new file mode 100644 index 0000000..b7b60ae --- /dev/null +++ b/apps/order-service/nest-cli.json @@ -0,0 +1,10 @@ +{ + "sourceRoot": "src", + "compilerOptions": { + "assets": [ + { + "include": "swagger/**/*" + } + ] + } +} diff --git a/apps/order-service/package.json b/apps/order-service/package.json new file mode 100644 index 0000000..2d60fd1 --- /dev/null +++ b/apps/order-service/package.json @@ -0,0 +1,79 @@ +{ + "name": "@order-service/server", + "private": true, + "scripts": { + "start": "nest start", + "start:watch": "nest start --watch", + "start:debug": "nest start --debug --watch", + "build": "nest build", + "test": "jest", + "seed": "ts-node scripts/seed.ts", + "db:migrate-save": "prisma migrate dev", + "db:migrate-up": "prisma migrate deploy", + "db:clean": "prisma migrate reset", + "db:init": "run-s \"db:migrate-save -- --name 'initial version'\" db:migrate-up seed", + "prisma:generate": "prisma generate", + "docker:dev": "docker-compose -f docker-compose.dev.yml up -d", + "package:container": "docker build .", + "compose:up": "docker-compose up -d", + "compose:down": "docker-compose down --volumes" + }, + "dependencies": { + "@apollo/server": "^4.9.4", + "@nestjs/apollo": "12.0.9", + "@nestjs/common": "10.2.7", + "@nestjs/config": "3.1.1", + "@nestjs/core": "10.2.7", + "@nestjs/graphql": "12.0.9", + "@nestjs/jwt": "^10.1.1", + "@nestjs/microservices": "10.2.7", + "@nestjs/passport": "^10.0.2", + "@nestjs/platform-express": "10.2.7", + "@nestjs/serve-static": "4.0.0", + "@nestjs/swagger": "7.1.13", + "@prisma/client": "^5.4.2", + "@types/bcrypt": "5.0.0", + "bcrypt": "5.1.1", + "class-transformer": "0.5.1", + "class-validator": "0.14.0", + "dotenv": "16.3.1", + "graphql": "^16.8.1", + "graphql-type-json": "0.3.2", + "kafkajs": "^2.2.4", + "nest-access-control": "^3.1.0", + "npm-run-all": "4.1.5", + "passport": "0.6.0", + "passport-http": "0.3.0", + "passport-jwt": "4.0.1", + "reflect-metadata": "0.1.13", + "ts-node": "10.9.2", + "type-fest": "2.19.0", + "validator": "13.11.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.1.18", + "@nestjs/testing": "^10.2.7", + "@types/express": "^4.17.19", + "@types/graphql-type-json": "0.3.3", + "@types/jest": "^29.5.5", + "@types/normalize-path": "3.0.0", + "@types/passport-http": "0.3.9", + "@types/passport-jwt": "3.0.10", + "@types/supertest": "^2.0.14", + "@types/validator": "^13.11.2", + "jest": "^29.7.0", + "jest-mock-extended": "^3.0.5", + "prisma": "^5.4.2", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", + "typescript": "^5.4.3" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "moduleNameMapper": { + "@app/custom-validators": "<rootDir>/src/validators" + }, + "modulePathIgnorePatterns": ["<rootDir>/dist/"] + } +} diff --git a/apps/order-service/prisma/schema.prisma b/apps/order-service/prisma/schema.prisma new file mode 100644 index 0000000..133605c --- /dev/null +++ b/apps/order-service/prisma/schema.prisma @@ -0,0 +1,67 @@ +datasource db { + provider = "postgresql" + url = env("DB_URL") +} + +generator client { + provider = "prisma-client-js" +} + +model User { + createdAt DateTime @default(now()) + firstName String? + id String @id @default(cuid()) + lastName String? + password String + roles Json + updatedAt DateTime @updatedAt + username String @unique +} + +model Order { + createdAt DateTime @default(now()) + customer Customer? + discount Float? + id String @id @default(cuid()) + product Product? @relation(fields: [productId], references: [id]) + quantity Int? + totalPrice Int? + updatedAt DateTime @updatedAt + productId String? +} + +model Customer { + address Address? + createdAt DateTime @default(now()) + email String? + firstName String? + id String @id @default(cuid()) + lastName String? + orders Order? @relation(fields: [orderId], references: [id]) + phone String? + updatedAt DateTime @updatedAt + orderId String? +} + +model Address { + address_1 String? + address_2 String? + city String? + createdAt DateTime @default(now()) + customers Customer? @relation(fields: [customerId], references: [id]) + id String @id @default(cuid()) + state String? + updatedAt DateTime @updatedAt + zip Int? + customerId String? +} + +model Product { + createdAt DateTime @default(now()) + description String? + id String @id @default(cuid()) + itemPrice Float? + name String? + orders Order? + updatedAt DateTime @updatedAt +} diff --git a/apps/order-service/scripts/customSeed.ts b/apps/order-service/scripts/customSeed.ts new file mode 100644 index 0000000..6baf5f6 --- /dev/null +++ b/apps/order-service/scripts/customSeed.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from "@prisma/client"; + +export async function customSeed() { + const client = new PrismaClient(); + const username = "admin"; + + //replace this sample code to populate your database + //with data that is required for your service to start + await client.user.update({ + where: { username: username }, + data: { + username, + }, + }); + + client.$disconnect(); +} diff --git a/apps/order-service/scripts/seed.ts b/apps/order-service/scripts/seed.ts new file mode 100644 index 0000000..272dddd --- /dev/null +++ b/apps/order-service/scripts/seed.ts @@ -0,0 +1,49 @@ +import * as dotenv from "dotenv"; +import { PrismaClient } from "@prisma/client"; +import { customSeed } from "./customSeed"; +import { Salt, parseSalt } from "../src/auth/password.service"; +import { hash } from "bcrypt"; + +if (require.main === module) { + dotenv.config(); + + const { BCRYPT_SALT } = process.env; + + if (!BCRYPT_SALT) { + throw new Error("BCRYPT_SALT environment variable must be defined"); + } + const salt = parseSalt(BCRYPT_SALT); + + seed(salt).catch((error) => { + console.error(error); + process.exit(1); + }); +} + +async function seed(bcryptSalt: Salt) { + console.info("Seeding database..."); + + const client = new PrismaClient(); + + const data = { + username: "admin", + password: await hash("admin", bcryptSalt), + roles: ["user"], + }; + + await client.user.upsert({ + where: { + username: data.username, + }, + + update: {}, + create: data, + }); + + void client.$disconnect(); + + console.info("Seeding database with custom seed..."); + customSeed(); + + console.info("Seeded database successfully"); +} diff --git a/apps/order-service/src/address/address.controller.ts b/apps/order-service/src/address/address.controller.ts new file mode 100644 index 0000000..feb5ea1 --- /dev/null +++ b/apps/order-service/src/address/address.controller.ts @@ -0,0 +1,17 @@ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import * as nestAccessControl from "nest-access-control"; +import { AddressService } from "./address.service"; +import { AddressControllerBase } from "./base/address.controller.base"; + +@swagger.ApiTags("addresses") +@common.Controller("addresses") +export class AddressController extends AddressControllerBase { + constructor( + protected readonly service: AddressService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/address/address.module.ts b/apps/order-service/src/address/address.module.ts new file mode 100644 index 0000000..1d1f733 --- /dev/null +++ b/apps/order-service/src/address/address.module.ts @@ -0,0 +1,14 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module"; +import { AddressModuleBase } from "./base/address.module.base"; +import { AddressService } from "./address.service"; +import { AddressController } from "./address.controller"; +import { AddressResolver } from "./address.resolver"; + +@Module({ + imports: [AddressModuleBase, forwardRef(() => AuthModule)], + controllers: [AddressController], + providers: [AddressService, AddressResolver], + exports: [AddressService], +}) +export class AddressModule {} diff --git a/apps/order-service/src/address/address.resolver.ts b/apps/order-service/src/address/address.resolver.ts new file mode 100644 index 0000000..f488247 --- /dev/null +++ b/apps/order-service/src/address/address.resolver.ts @@ -0,0 +1,20 @@ +import * as graphql from "@nestjs/graphql"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { AddressResolverBase } from "./base/address.resolver.base"; +import { Address } from "./base/Address"; +import { AddressService } from "./address.service"; + +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Address) +export class AddressResolver extends AddressResolverBase { + constructor( + protected readonly service: AddressService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/address/address.service.ts b/apps/order-service/src/address/address.service.ts new file mode 100644 index 0000000..9a1741e --- /dev/null +++ b/apps/order-service/src/address/address.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { AddressServiceBase } from "./base/address.service.base"; + +@Injectable() +export class AddressService extends AddressServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/apps/order-service/src/address/base/Address.ts b/apps/order-service/src/address/base/Address.ts new file mode 100644 index 0000000..d5688f4 --- /dev/null +++ b/apps/order-service/src/address/base/Address.ts @@ -0,0 +1,124 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { + IsString, + MaxLength, + IsOptional, + IsDate, + ValidateNested, + IsInt, + Min, + Max, +} from "class-validator"; +import { Type } from "class-transformer"; +import { Customer } from "../../customer/base/Customer"; + +@ObjectType() +class Address { + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + address_1!: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + address_2!: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + city!: string | null; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + createdAt!: Date; + + @ApiProperty({ + required: false, + type: () => Customer, + }) + @ValidateNested() + @Type(() => Customer) + @IsOptional() + customers?: Customer | null; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + state!: string | null; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + updatedAt!: Date; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + zip!: number | null; +} + +export { Address as Address }; diff --git a/apps/order-service/src/address/base/AddressCountArgs.ts b/apps/order-service/src/address/base/AddressCountArgs.ts new file mode 100644 index 0000000..62ea4a8 --- /dev/null +++ b/apps/order-service/src/address/base/AddressCountArgs.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereInput } from "./AddressWhereInput"; +import { Type } from "class-transformer"; + +@ArgsType() +class AddressCountArgs { + @ApiProperty({ + required: false, + type: () => AddressWhereInput, + }) + @Field(() => AddressWhereInput, { nullable: true }) + @Type(() => AddressWhereInput) + where?: AddressWhereInput; +} + +export { AddressCountArgs as AddressCountArgs }; diff --git a/apps/order-service/src/address/base/AddressCreateInput.ts b/apps/order-service/src/address/base/AddressCreateInput.ts new file mode 100644 index 0000000..8c7658a --- /dev/null +++ b/apps/order-service/src/address/base/AddressCreateInput.ts @@ -0,0 +1,102 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { + IsString, + MaxLength, + IsOptional, + ValidateNested, + IsInt, + Min, + Max, +} from "class-validator"; +import { CustomerWhereUniqueInput } from "../../customer/base/CustomerWhereUniqueInput"; +import { Type } from "class-transformer"; + +@InputType() +class AddressCreateInput { + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + address_1?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + address_2?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + city?: string | null; + + @ApiProperty({ + required: false, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @IsOptional() + @Field(() => CustomerWhereUniqueInput, { + nullable: true, + }) + customers?: CustomerWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + state?: string | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + zip?: number | null; +} + +export { AddressCreateInput as AddressCreateInput }; diff --git a/apps/order-service/src/address/base/AddressFindManyArgs.ts b/apps/order-service/src/address/base/AddressFindManyArgs.ts new file mode 100644 index 0000000..52e23ab --- /dev/null +++ b/apps/order-service/src/address/base/AddressFindManyArgs.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereInput } from "./AddressWhereInput"; +import { IsOptional, ValidateNested, IsInt } from "class-validator"; +import { Type } from "class-transformer"; +import { AddressOrderByInput } from "./AddressOrderByInput"; + +@ArgsType() +class AddressFindManyArgs { + @ApiProperty({ + required: false, + type: () => AddressWhereInput, + }) + @IsOptional() + @ValidateNested() + @Field(() => AddressWhereInput, { nullable: true }) + @Type(() => AddressWhereInput) + where?: AddressWhereInput; + + @ApiProperty({ + required: false, + type: [AddressOrderByInput], + }) + @IsOptional() + @ValidateNested({ each: true }) + @Field(() => [AddressOrderByInput], { nullable: true }) + @Type(() => AddressOrderByInput) + orderBy?: Array<AddressOrderByInput>; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + skip?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + take?: number; +} + +export { AddressFindManyArgs as AddressFindManyArgs }; diff --git a/apps/order-service/src/address/base/AddressFindUniqueArgs.ts b/apps/order-service/src/address/base/AddressFindUniqueArgs.ts new file mode 100644 index 0000000..cb8fb47 --- /dev/null +++ b/apps/order-service/src/address/base/AddressFindUniqueArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereUniqueInput } from "./AddressWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class AddressFindUniqueArgs { + @ApiProperty({ + required: true, + type: () => AddressWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AddressWhereUniqueInput) + @Field(() => AddressWhereUniqueInput, { nullable: false }) + where!: AddressWhereUniqueInput; +} + +export { AddressFindUniqueArgs as AddressFindUniqueArgs }; diff --git a/apps/order-service/src/address/base/AddressListRelationFilter.ts b/apps/order-service/src/address/base/AddressListRelationFilter.ts new file mode 100644 index 0000000..e0b3d07 --- /dev/null +++ b/apps/order-service/src/address/base/AddressListRelationFilter.ts @@ -0,0 +1,56 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereInput } from "./AddressWhereInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class AddressListRelationFilter { + @ApiProperty({ + required: false, + type: () => AddressWhereInput, + }) + @ValidateNested() + @Type(() => AddressWhereInput) + @IsOptional() + @Field(() => AddressWhereInput, { + nullable: true, + }) + every?: AddressWhereInput; + + @ApiProperty({ + required: false, + type: () => AddressWhereInput, + }) + @ValidateNested() + @Type(() => AddressWhereInput) + @IsOptional() + @Field(() => AddressWhereInput, { + nullable: true, + }) + some?: AddressWhereInput; + + @ApiProperty({ + required: false, + type: () => AddressWhereInput, + }) + @ValidateNested() + @Type(() => AddressWhereInput) + @IsOptional() + @Field(() => AddressWhereInput, { + nullable: true, + }) + none?: AddressWhereInput; +} +export { AddressListRelationFilter as AddressListRelationFilter }; diff --git a/apps/order-service/src/address/base/AddressOrderByInput.ts b/apps/order-service/src/address/base/AddressOrderByInput.ts new file mode 100644 index 0000000..db1aad9 --- /dev/null +++ b/apps/order-service/src/address/base/AddressOrderByInput.ts @@ -0,0 +1,122 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsEnum } from "class-validator"; +import { SortOrder } from "../../util/SortOrder"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +class AddressOrderByInput { + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + address_1?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + address_2?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + city?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + createdAt?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + customersId?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + id?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + state?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + updatedAt?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + zip?: SortOrder; +} + +export { AddressOrderByInput as AddressOrderByInput }; diff --git a/apps/order-service/src/address/base/AddressUpdateInput.ts b/apps/order-service/src/address/base/AddressUpdateInput.ts new file mode 100644 index 0000000..05604e2 --- /dev/null +++ b/apps/order-service/src/address/base/AddressUpdateInput.ts @@ -0,0 +1,102 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { + IsString, + MaxLength, + IsOptional, + ValidateNested, + IsInt, + Min, + Max, +} from "class-validator"; +import { CustomerWhereUniqueInput } from "../../customer/base/CustomerWhereUniqueInput"; +import { Type } from "class-transformer"; + +@InputType() +class AddressUpdateInput { + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + address_1?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + address_2?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + city?: string | null; + + @ApiProperty({ + required: false, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @IsOptional() + @Field(() => CustomerWhereUniqueInput, { + nullable: true, + }) + customers?: CustomerWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + state?: string | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + zip?: number | null; +} + +export { AddressUpdateInput as AddressUpdateInput }; diff --git a/apps/order-service/src/address/base/AddressWhereInput.ts b/apps/order-service/src/address/base/AddressWhereInput.ts new file mode 100644 index 0000000..65f05e8 --- /dev/null +++ b/apps/order-service/src/address/base/AddressWhereInput.ts @@ -0,0 +1,102 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { Type } from "class-transformer"; +import { IsOptional, ValidateNested } from "class-validator"; +import { CustomerWhereUniqueInput } from "../../customer/base/CustomerWhereUniqueInput"; +import { StringFilter } from "../../util/StringFilter"; +import { IntNullableFilter } from "../../util/IntNullableFilter"; + +@InputType() +class AddressWhereInput { + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + address_1?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + address_2?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + city?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @IsOptional() + @Field(() => CustomerWhereUniqueInput, { + nullable: true, + }) + customers?: CustomerWhereUniqueInput; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + id?: StringFilter; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + state?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: IntNullableFilter, + }) + @Type(() => IntNullableFilter) + @IsOptional() + @Field(() => IntNullableFilter, { + nullable: true, + }) + zip?: IntNullableFilter; +} + +export { AddressWhereInput as AddressWhereInput }; diff --git a/apps/order-service/src/address/base/AddressWhereUniqueInput.ts b/apps/order-service/src/address/base/AddressWhereUniqueInput.ts new file mode 100644 index 0000000..7493f2c --- /dev/null +++ b/apps/order-service/src/address/base/AddressWhereUniqueInput.ts @@ -0,0 +1,27 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +@InputType() +class AddressWhereUniqueInput { + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; +} + +export { AddressWhereUniqueInput as AddressWhereUniqueInput }; diff --git a/apps/order-service/src/address/base/CreateAddressArgs.ts b/apps/order-service/src/address/base/CreateAddressArgs.ts new file mode 100644 index 0000000..2608ab3 --- /dev/null +++ b/apps/order-service/src/address/base/CreateAddressArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressCreateInput } from "./AddressCreateInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CreateAddressArgs { + @ApiProperty({ + required: true, + type: () => AddressCreateInput, + }) + @ValidateNested() + @Type(() => AddressCreateInput) + @Field(() => AddressCreateInput, { nullable: false }) + data!: AddressCreateInput; +} + +export { CreateAddressArgs as CreateAddressArgs }; diff --git a/apps/order-service/src/address/base/DeleteAddressArgs.ts b/apps/order-service/src/address/base/DeleteAddressArgs.ts new file mode 100644 index 0000000..354c3f0 --- /dev/null +++ b/apps/order-service/src/address/base/DeleteAddressArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereUniqueInput } from "./AddressWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class DeleteAddressArgs { + @ApiProperty({ + required: true, + type: () => AddressWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AddressWhereUniqueInput) + @Field(() => AddressWhereUniqueInput, { nullable: false }) + where!: AddressWhereUniqueInput; +} + +export { DeleteAddressArgs as DeleteAddressArgs }; diff --git a/apps/order-service/src/address/base/UpdateAddressArgs.ts b/apps/order-service/src/address/base/UpdateAddressArgs.ts new file mode 100644 index 0000000..1eb6576 --- /dev/null +++ b/apps/order-service/src/address/base/UpdateAddressArgs.ts @@ -0,0 +1,40 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereUniqueInput } from "./AddressWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { AddressUpdateInput } from "./AddressUpdateInput"; + +@ArgsType() +class UpdateAddressArgs { + @ApiProperty({ + required: true, + type: () => AddressWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AddressWhereUniqueInput) + @Field(() => AddressWhereUniqueInput, { nullable: false }) + where!: AddressWhereUniqueInput; + + @ApiProperty({ + required: true, + type: () => AddressUpdateInput, + }) + @ValidateNested() + @Type(() => AddressUpdateInput) + @Field(() => AddressUpdateInput, { nullable: false }) + data!: AddressUpdateInput; +} + +export { UpdateAddressArgs as UpdateAddressArgs }; diff --git a/apps/order-service/src/address/base/address.controller.base.spec.ts b/apps/order-service/src/address/base/address.controller.base.spec.ts new file mode 100644 index 0000000..b4e55e1 --- /dev/null +++ b/apps/order-service/src/address/base/address.controller.base.spec.ts @@ -0,0 +1,210 @@ +import { Test } from "@nestjs/testing"; +import { + INestApplication, + HttpStatus, + ExecutionContext, + CallHandler, +} from "@nestjs/common"; +import request from "supertest"; +import { ACGuard } from "nest-access-control"; +import { DefaultAuthGuard } from "../../auth/defaultAuth.guard"; +import { ACLModule } from "../../auth/acl.module"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { map } from "rxjs"; +import { AddressController } from "../address.controller"; +import { AddressService } from "../address.service"; + +const nonExistingId = "nonExistingId"; +const existingId = "existingId"; +const CREATE_INPUT = { + address_1: "exampleAddress_1", + address_2: "exampleAddress_2", + city: "exampleCity", + createdAt: new Date(), + id: "exampleId", + state: "exampleState", + updatedAt: new Date(), + zip: 42, +}; +const CREATE_RESULT = { + address_1: "exampleAddress_1", + address_2: "exampleAddress_2", + city: "exampleCity", + createdAt: new Date(), + id: "exampleId", + state: "exampleState", + updatedAt: new Date(), + zip: 42, +}; +const FIND_MANY_RESULT = [ + { + address_1: "exampleAddress_1", + address_2: "exampleAddress_2", + city: "exampleCity", + createdAt: new Date(), + id: "exampleId", + state: "exampleState", + updatedAt: new Date(), + zip: 42, + }, +]; +const FIND_ONE_RESULT = { + address_1: "exampleAddress_1", + address_2: "exampleAddress_2", + city: "exampleCity", + createdAt: new Date(), + id: "exampleId", + state: "exampleState", + updatedAt: new Date(), + zip: 42, +}; + +const service = { + createAddress() { + return CREATE_RESULT; + }, + addresses: () => FIND_MANY_RESULT, + address: ({ where }: { where: { id: string } }) => { + switch (where.id) { + case existingId: + return FIND_ONE_RESULT; + case nonExistingId: + return null; + } + }, +}; + +const basicAuthGuard = { + canActivate: (context: ExecutionContext) => { + const argumentHost = context.switchToHttp(); + const request = argumentHost.getRequest(); + request.user = { + roles: ["user"], + }; + return true; + }, +}; + +const acGuard = { + canActivate: () => { + return true; + }, +}; + +const aclFilterResponseInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle().pipe( + map((data) => { + return data; + }) + ); + }, +}; +const aclValidateRequestInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle(); + }, +}; + +describe("Address", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: AddressService, + useValue: service, + }, + ], + controllers: [AddressController], + imports: [ACLModule], + }) + .overrideGuard(DefaultAuthGuard) + .useValue(basicAuthGuard) + .overrideGuard(ACGuard) + .useValue(acGuard) + .overrideInterceptor(AclFilterResponseInterceptor) + .useValue(aclFilterResponseInterceptor) + .overrideInterceptor(AclValidateRequestInterceptor) + .useValue(aclValidateRequestInterceptor) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + test("POST /addresses", async () => { + await request(app.getHttpServer()) + .post("/addresses") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }); + }); + + test("GET /addresses", async () => { + await request(app.getHttpServer()) + .get("/addresses") + .expect(HttpStatus.OK) + .expect([ + { + ...FIND_MANY_RESULT[0], + createdAt: FIND_MANY_RESULT[0].createdAt.toISOString(), + updatedAt: FIND_MANY_RESULT[0].updatedAt.toISOString(), + }, + ]); + }); + + test("GET /addresses/:id non existing", async () => { + await request(app.getHttpServer()) + .get(`${"/addresses"}/${nonExistingId}`) + .expect(HttpStatus.NOT_FOUND) + .expect({ + statusCode: HttpStatus.NOT_FOUND, + message: `No resource was found for {"${"id"}":"${nonExistingId}"}`, + error: "Not Found", + }); + }); + + test("GET /addresses/:id existing", async () => { + await request(app.getHttpServer()) + .get(`${"/addresses"}/${existingId}`) + .expect(HttpStatus.OK) + .expect({ + ...FIND_ONE_RESULT, + createdAt: FIND_ONE_RESULT.createdAt.toISOString(), + updatedAt: FIND_ONE_RESULT.updatedAt.toISOString(), + }); + }); + + test("POST /addresses existing resource", async () => { + const agent = request(app.getHttpServer()); + await agent + .post("/addresses") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }) + .then(function () { + agent + .post("/addresses") + .send(CREATE_INPUT) + .expect(HttpStatus.CONFLICT) + .expect({ + statusCode: HttpStatus.CONFLICT, + }); + }); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/apps/order-service/src/address/base/address.controller.base.ts b/apps/order-service/src/address/base/address.controller.base.ts new file mode 100644 index 0000000..d77bd87 --- /dev/null +++ b/apps/order-service/src/address/base/address.controller.base.ts @@ -0,0 +1,260 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { isRecordNotFoundError } from "../../prisma.util"; +import * as errors from "../../errors"; +import { Request } from "express"; +import { plainToClass } from "class-transformer"; +import { ApiNestedQuery } from "../../decorators/api-nested-query.decorator"; +import * as nestAccessControl from "nest-access-control"; +import * as defaultAuthGuard from "../../auth/defaultAuth.guard"; +import { AddressService } from "../address.service"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AddressCreateInput } from "./AddressCreateInput"; +import { Address } from "./Address"; +import { AddressFindManyArgs } from "./AddressFindManyArgs"; +import { AddressWhereUniqueInput } from "./AddressWhereUniqueInput"; +import { AddressUpdateInput } from "./AddressUpdateInput"; + +@swagger.ApiBearerAuth() +@common.UseGuards(defaultAuthGuard.DefaultAuthGuard, nestAccessControl.ACGuard) +export class AddressControllerBase { + constructor( + protected readonly service: AddressService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Post() + @swagger.ApiCreatedResponse({ type: Address }) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "create", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async createAddress( + @common.Body() data: AddressCreateInput + ): Promise<Address> { + return await this.service.createAddress({ + data: { + ...data, + + customers: data.customers + ? { + connect: data.customers, + } + : undefined, + }, + select: { + address_1: true, + address_2: true, + city: true, + createdAt: true, + + customers: { + select: { + id: true, + }, + }, + + id: true, + state: true, + updatedAt: true, + zip: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get() + @swagger.ApiOkResponse({ type: [Address] }) + @ApiNestedQuery(AddressFindManyArgs) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "read", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async addresses(@common.Req() request: Request): Promise<Address[]> { + const args = plainToClass(AddressFindManyArgs, request.query); + return this.service.addresses({ + ...args, + select: { + address_1: true, + address_2: true, + city: true, + createdAt: true, + + customers: { + select: { + id: true, + }, + }, + + id: true, + state: true, + updatedAt: true, + zip: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get("/:id") + @swagger.ApiOkResponse({ type: Address }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "read", + possession: "own", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async address( + @common.Param() params: AddressWhereUniqueInput + ): Promise<Address | null> { + const result = await this.service.address({ + where: params, + select: { + address_1: true, + address_2: true, + city: true, + createdAt: true, + + customers: { + select: { + id: true, + }, + }, + + id: true, + state: true, + updatedAt: true, + zip: true, + }, + }); + if (result === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Patch("/:id") + @swagger.ApiOkResponse({ type: Address }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "update", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async updateAddress( + @common.Param() params: AddressWhereUniqueInput, + @common.Body() data: AddressUpdateInput + ): Promise<Address | null> { + try { + return await this.service.updateAddress({ + where: params, + data: { + ...data, + + customers: data.customers + ? { + connect: data.customers, + } + : undefined, + }, + select: { + address_1: true, + address_2: true, + city: true, + createdAt: true, + + customers: { + select: { + id: true, + }, + }, + + id: true, + state: true, + updatedAt: true, + zip: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Delete("/:id") + @swagger.ApiOkResponse({ type: Address }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "delete", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async deleteAddress( + @common.Param() params: AddressWhereUniqueInput + ): Promise<Address | null> { + try { + return await this.service.deleteAddress({ + where: params, + select: { + address_1: true, + address_2: true, + city: true, + createdAt: true, + + customers: { + select: { + id: true, + }, + }, + + id: true, + state: true, + updatedAt: true, + zip: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } +} diff --git a/apps/order-service/src/address/base/address.module.base.ts b/apps/order-service/src/address/base/address.module.base.ts new file mode 100644 index 0000000..761dd8e --- /dev/null +++ b/apps/order-service/src/address/base/address.module.base.ts @@ -0,0 +1,18 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { Module } from "@nestjs/common"; +import { ACLModule } from "../../auth/acl.module"; +@Module({ + imports: [ACLModule], + exports: [ACLModule], +}) +export class AddressModuleBase {} diff --git a/apps/order-service/src/address/base/address.resolver.base.ts b/apps/order-service/src/address/base/address.resolver.base.ts new file mode 100644 index 0000000..daea377 --- /dev/null +++ b/apps/order-service/src/address/base/address.resolver.base.ts @@ -0,0 +1,182 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as graphql from "@nestjs/graphql"; +import { GraphQLError } from "graphql"; +import { isRecordNotFoundError } from "../../prisma.util"; +import { MetaQueryPayload } from "../../util/MetaQueryPayload"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { Address } from "./Address"; +import { AddressCountArgs } from "./AddressCountArgs"; +import { AddressFindManyArgs } from "./AddressFindManyArgs"; +import { AddressFindUniqueArgs } from "./AddressFindUniqueArgs"; +import { CreateAddressArgs } from "./CreateAddressArgs"; +import { UpdateAddressArgs } from "./UpdateAddressArgs"; +import { DeleteAddressArgs } from "./DeleteAddressArgs"; +import { Customer } from "../../customer/base/Customer"; +import { AddressService } from "../address.service"; +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Address) +export class AddressResolverBase { + constructor( + protected readonly service: AddressService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + + @graphql.Query(() => MetaQueryPayload) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "read", + possession: "any", + }) + async _addressesMeta( + @graphql.Args() args: AddressCountArgs + ): Promise<MetaQueryPayload> { + const result = await this.service.count(args); + return { + count: result, + }; + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => [Address]) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "read", + possession: "any", + }) + async addresses( + @graphql.Args() args: AddressFindManyArgs + ): Promise<Address[]> { + return this.service.addresses(args); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => Address, { nullable: true }) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "read", + possession: "own", + }) + async address( + @graphql.Args() args: AddressFindUniqueArgs + ): Promise<Address | null> { + const result = await this.service.address(args); + if (result === null) { + return null; + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Address) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "create", + possession: "any", + }) + async createAddress( + @graphql.Args() args: CreateAddressArgs + ): Promise<Address> { + return await this.service.createAddress({ + ...args, + data: { + ...args.data, + + customers: args.data.customers + ? { + connect: args.data.customers, + } + : undefined, + }, + }); + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Address) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "update", + possession: "any", + }) + async updateAddress( + @graphql.Args() args: UpdateAddressArgs + ): Promise<Address | null> { + try { + return await this.service.updateAddress({ + ...args, + data: { + ...args.data, + + customers: args.data.customers + ? { + connect: args.data.customers, + } + : undefined, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.Mutation(() => Address) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "delete", + possession: "any", + }) + async deleteAddress( + @graphql.Args() args: DeleteAddressArgs + ): Promise<Address | null> { + try { + return await this.service.deleteAddress(args); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.ResolveField(() => Customer, { + nullable: true, + name: "customers", + }) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "read", + possession: "any", + }) + async getCustomers( + @graphql.Parent() parent: Address + ): Promise<Customer | null> { + const result = await this.service.getCustomers(parent.id); + + if (!result) { + return null; + } + return result; + } +} diff --git a/apps/order-service/src/address/base/address.service.base.ts b/apps/order-service/src/address/base/address.service.base.ts new file mode 100644 index 0000000..e26ea02 --- /dev/null +++ b/apps/order-service/src/address/base/address.service.base.ts @@ -0,0 +1,51 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { PrismaService } from "../../prisma/prisma.service"; +import { + Prisma, + Address as PrismaAddress, + Customer as PrismaCustomer, +} from "@prisma/client"; + +export class AddressServiceBase { + constructor(protected readonly prisma: PrismaService) {} + + async count(args: Omit<Prisma.AddressCountArgs, "select">): Promise<number> { + return this.prisma.address.count(args); + } + + async addresses(args: Prisma.AddressFindManyArgs): Promise<PrismaAddress[]> { + return this.prisma.address.findMany(args); + } + async address( + args: Prisma.AddressFindUniqueArgs + ): Promise<PrismaAddress | null> { + return this.prisma.address.findUnique(args); + } + async createAddress(args: Prisma.AddressCreateArgs): Promise<PrismaAddress> { + return this.prisma.address.create(args); + } + async updateAddress(args: Prisma.AddressUpdateArgs): Promise<PrismaAddress> { + return this.prisma.address.update(args); + } + async deleteAddress(args: Prisma.AddressDeleteArgs): Promise<PrismaAddress> { + return this.prisma.address.delete(args); + } + + async getCustomers(parentId: string): Promise<PrismaCustomer | null> { + return this.prisma.address + .findUnique({ + where: { id: parentId }, + }) + .customers(); + } +} diff --git a/apps/order-service/src/app.module.ts b/apps/order-service/src/app.module.ts new file mode 100644 index 0000000..49be2fa --- /dev/null +++ b/apps/order-service/src/app.module.ts @@ -0,0 +1,56 @@ +import { Module } from "@nestjs/common"; +import { UserModule } from "./user/user.module"; +import { OrderModule } from "./order/order.module"; +import { CustomerModule } from "./customer/customer.module"; +import { AddressModule } from "./address/address.module"; +import { ProductModule } from "./product/product.module"; +import { HealthModule } from "./health/health.module"; +import { PrismaModule } from "./prisma/prisma.module"; +import { SecretsManagerModule } from "./providers/secrets/secretsManager.module"; +import { KafkaModule } from "./kafka/kafka.module"; +import { ServeStaticModule } from "@nestjs/serve-static"; +import { ServeStaticOptionsService } from "./serveStaticOptions.service"; +import { ConfigModule, ConfigService } from "@nestjs/config"; +import { GraphQLModule } from "@nestjs/graphql"; +import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo"; + +import { ACLModule } from "./auth/acl.module"; +import { AuthModule } from "./auth/auth.module"; + +@Module({ + controllers: [], + imports: [ + KafkaModule, + ACLModule, + AuthModule, + UserModule, + OrderModule, + CustomerModule, + AddressModule, + ProductModule, + HealthModule, + PrismaModule, + SecretsManagerModule, + ConfigModule.forRoot({ isGlobal: true }), + ServeStaticModule.forRootAsync({ + useClass: ServeStaticOptionsService, + }), + GraphQLModule.forRootAsync<ApolloDriverConfig>({ + driver: ApolloDriver, + useFactory: (configService: ConfigService) => { + const playground = configService.get("GRAPHQL_PLAYGROUND"); + const introspection = configService.get("GRAPHQL_INTROSPECTION"); + return { + autoSchemaFile: "schema.graphql", + sortSchema: true, + playground, + introspection: playground || introspection, + }; + }, + inject: [ConfigService], + imports: [ConfigModule], + }), + ], + providers: [], +}) +export class AppModule {} diff --git a/apps/order-service/src/auth/Credentials.ts b/apps/order-service/src/auth/Credentials.ts new file mode 100644 index 0000000..9ac6798 --- /dev/null +++ b/apps/order-service/src/auth/Credentials.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { InputType, Field } from "@nestjs/graphql"; +import { IsString } from "class-validator"; + +@InputType() +export class Credentials { + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String, { nullable: false }) + username!: string; + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String, { nullable: false }) + password!: string; +} diff --git a/apps/order-service/src/auth/IAuthStrategy.ts b/apps/order-service/src/auth/IAuthStrategy.ts new file mode 100644 index 0000000..7406267 --- /dev/null +++ b/apps/order-service/src/auth/IAuthStrategy.ts @@ -0,0 +1,6 @@ +import { UserInfo } from "./UserInfo"; + +export interface IAuthStrategy { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validate: (...any: any) => Promise<UserInfo>; +} diff --git a/apps/order-service/src/auth/ITokenService.ts b/apps/order-service/src/auth/ITokenService.ts new file mode 100644 index 0000000..7983189 --- /dev/null +++ b/apps/order-service/src/auth/ITokenService.ts @@ -0,0 +1,9 @@ +export interface ITokenPayload { + id: string; + username: string; + password: string; +} + +export interface ITokenService { + createToken: ({ id, username, password }: ITokenPayload) => Promise<string>; +} diff --git a/apps/order-service/src/auth/LoginArgs.ts b/apps/order-service/src/auth/LoginArgs.ts new file mode 100644 index 0000000..66a0b2f --- /dev/null +++ b/apps/order-service/src/auth/LoginArgs.ts @@ -0,0 +1,12 @@ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { Credentials } from "./Credentials"; + +@ArgsType() +export class LoginArgs { + @Field(() => Credentials, { nullable: false }) + @Type(() => Credentials) + @ValidateNested() + credentials!: Credentials; +} diff --git a/apps/order-service/src/auth/UserInfo.ts b/apps/order-service/src/auth/UserInfo.ts new file mode 100644 index 0000000..ef61dc4 --- /dev/null +++ b/apps/order-service/src/auth/UserInfo.ts @@ -0,0 +1,14 @@ +import { Field, ObjectType } from "@nestjs/graphql"; +import { User } from "../user/base/User"; + +@ObjectType() +export class UserInfo implements Partial<User> { + @Field(() => String) + id!: string; + @Field(() => String) + username!: string; + @Field(() => [String]) + roles!: string[]; + @Field(() => String, { nullable: true }) + accessToken?: string; +} diff --git a/apps/order-service/src/auth/abac.util.ts b/apps/order-service/src/auth/abac.util.ts new file mode 100644 index 0000000..2f0dcab --- /dev/null +++ b/apps/order-service/src/auth/abac.util.ts @@ -0,0 +1,19 @@ +import { Permission } from "accesscontrol"; + +/** + * @returns attributes not allowed to appear on given data according to given + * attributeMatchers + */ +export function getInvalidAttributes( + permission: Permission, + // eslint-disable-next-line @typescript-eslint/ban-types + data: Object +): string[] { + // The structuredClone call is necessary because the + // `Permission.filter` function doesn't consider objects + // with null prototypes. And in graphql requests, the + // object passed here by the request interceptor is an object + // with a null prototype. + const filteredData = permission.filter(structuredClone(data)); + return Object.keys(data).filter((key) => !(key in filteredData)); +} diff --git a/apps/order-service/src/auth/acl.module.ts b/apps/order-service/src/auth/acl.module.ts new file mode 100644 index 0000000..040e7e5 --- /dev/null +++ b/apps/order-service/src/auth/acl.module.ts @@ -0,0 +1,6 @@ +import { AccessControlModule, RolesBuilder } from "nest-access-control"; + +import grants from "../grants.json"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const ACLModule = AccessControlModule.forRoles(new RolesBuilder(grants)); diff --git a/apps/order-service/src/auth/auth.controller.ts b/apps/order-service/src/auth/auth.controller.ts new file mode 100644 index 0000000..7ae732f --- /dev/null +++ b/apps/order-service/src/auth/auth.controller.ts @@ -0,0 +1,15 @@ +import { Body, Controller, Post } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { AuthService } from "./auth.service"; +import { Credentials } from "../auth/Credentials"; +import { UserInfo } from "./UserInfo"; + +@ApiTags("auth") +@Controller() +export class AuthController { + constructor(private readonly authService: AuthService) {} + @Post("login") + async login(@Body() body: Credentials): Promise<UserInfo> { + return this.authService.login(body); + } +} diff --git a/apps/order-service/src/auth/auth.module.ts b/apps/order-service/src/auth/auth.module.ts new file mode 100644 index 0000000..d69fd95 --- /dev/null +++ b/apps/order-service/src/auth/auth.module.ts @@ -0,0 +1,57 @@ +import { forwardRef, Module } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { JwtModule } from "@nestjs/jwt"; +import { PassportModule } from "@nestjs/passport"; +import { JWT_EXPIRATION } from "../constants"; +import { SecretsManagerModule } from "../providers/secrets/secretsManager.module"; +import { SecretsManagerService } from "../providers/secrets/secretsManager.service"; +import { EnumSecretsNameKey } from "../providers/secrets/secretsNameKey.enum"; +import { AuthController } from "./auth.controller"; +import { AuthResolver } from "./auth.resolver"; +import { AuthService } from "./auth.service"; +import { JwtStrategy } from "./jwt/jwt.strategy"; +import { jwtSecretFactory } from "./jwt/jwtSecretFactory"; +import { PasswordService } from "./password.service"; +import { TokenService } from "./token.service"; +import { UserModule } from "../user/user.module"; +@Module({ + imports: [ + forwardRef(() => UserModule), + PassportModule, + SecretsManagerModule, + JwtModule.registerAsync({ + imports: [SecretsManagerModule], + inject: [SecretsManagerService, ConfigService], + useFactory: async ( + secretsService: SecretsManagerService, + configService: ConfigService + ) => { + const secret = await secretsService.getSecret<string>( + EnumSecretsNameKey.JwtSecretKey + ); + const expiresIn = configService.get(JWT_EXPIRATION); + if (!secret) { + throw new Error("Didn't get a valid jwt secret"); + } + if (!expiresIn) { + throw new Error("Jwt expire in value is not valid"); + } + return { + secret: secret, + signOptions: { expiresIn }, + }; + }, + }), + ], + providers: [ + AuthService, + PasswordService, + AuthResolver, + JwtStrategy, + jwtSecretFactory, + TokenService, + ], + controllers: [AuthController], + exports: [AuthService, PasswordService], +}) +export class AuthModule {} diff --git a/apps/order-service/src/auth/auth.resolver.ts b/apps/order-service/src/auth/auth.resolver.ts new file mode 100644 index 0000000..c186f41 --- /dev/null +++ b/apps/order-service/src/auth/auth.resolver.ts @@ -0,0 +1,23 @@ +import * as common from "@nestjs/common"; +import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; +import * as gqlACGuard from "../auth/gqlAC.guard"; +import { AuthService } from "./auth.service"; +import { GqlDefaultAuthGuard } from "./gqlDefaultAuth.guard"; +import { UserData } from "./userData.decorator"; +import { LoginArgs } from "./LoginArgs"; +import { UserInfo } from "./UserInfo"; + +@Resolver(UserInfo) +export class AuthResolver { + constructor(private readonly authService: AuthService) {} + @Mutation(() => UserInfo) + async login(@Args() args: LoginArgs): Promise<UserInfo> { + return this.authService.login(args.credentials); + } + + @Query(() => UserInfo) + @common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) + async userInfo(@UserData() entityInfo: UserInfo): Promise<UserInfo> { + return entityInfo; + } +} diff --git a/apps/order-service/src/auth/auth.service.spec.ts b/apps/order-service/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..90b04f4 --- /dev/null +++ b/apps/order-service/src/auth/auth.service.spec.ts @@ -0,0 +1,114 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { AuthService } from "./auth.service"; +import { Credentials } from "./Credentials"; +import { PasswordService } from "./password.service"; +import { TokenService } from "./token.service"; +import { VALID_ID } from "../tests/auth/constants"; +import { UserService } from "../user/user.service"; + +const VALID_CREDENTIALS: Credentials = { + username: "Valid User", + password: "Valid User Password", +}; +const INVALID_CREDENTIALS: Credentials = { + username: "Invalid User", + password: "Invalid User Password", +}; +const USER: any = { + ...VALID_CREDENTIALS, + createdAt: new Date(), + firstName: "ofek", + id: VALID_ID, + lastName: "gabay", + roles: ["admin"], + updatedAt: new Date(), +}; + +const SIGN_TOKEN = "SIGN_TOKEN"; + +const authEntityService = { + user(args: { where: { username: string } }): any | null { + if (args.where.username === VALID_CREDENTIALS.username) { + return USER; + } + return null; + }, +}; + +const passwordService = { + compare(password: string, encrypted: string) { + return true; + }, +}; + +const tokenService = { + createToken(username: string, password: string) { + return SIGN_TOKEN; + }, +}; + +describe("AuthService", () => { + //ARRANGE + let service: AuthService; + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: UserService, + useValue: authEntityService, + }, + { + provide: PasswordService, + useValue: passwordService, + }, + { + provide: TokenService, + useValue: tokenService, + }, + AuthService, + ], + }).compile(); + + service = module.get<AuthService>(AuthService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("Testing the authService.validateUser()", () => { + it("should validate a valid user", async () => { + await expect( + service.validateUser( + VALID_CREDENTIALS.username, + VALID_CREDENTIALS.password + ) + ).resolves.toEqual({ + username: USER.username, + roles: USER.roles, + id: USER.id, + }); + }); + + it("should not validate a invalid user", async () => { + await expect( + service.validateUser( + INVALID_CREDENTIALS.username, + INVALID_CREDENTIALS.password + ) + ).resolves.toBe(null); + }); + }); + + describe("Testing the authService.login()", () => { + it("should return userInfo object for correct username and password", async () => { + const loginResult = await service.login(VALID_CREDENTIALS); + expect(loginResult).toEqual({ + username: USER.username, + roles: USER.roles, + accessToken: SIGN_TOKEN, + id: USER.id, + }); + }); + }); +}); diff --git a/apps/order-service/src/auth/auth.service.ts b/apps/order-service/src/auth/auth.service.ts new file mode 100644 index 0000000..9f6add2 --- /dev/null +++ b/apps/order-service/src/auth/auth.service.ts @@ -0,0 +1,49 @@ +import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { Credentials } from "./Credentials"; +import { PasswordService } from "./password.service"; +import { TokenService } from "./token.service"; +import { UserInfo } from "./UserInfo"; +import { UserService } from "../user/user.service"; + +@Injectable() +export class AuthService { + constructor( + private readonly passwordService: PasswordService, + private readonly tokenService: TokenService, + private readonly userService: UserService + ) {} + + async validateUser( + username: string, + password: string + ): Promise<UserInfo | null> { + const user = await this.userService.user({ + where: { username }, + }); + if (user && (await this.passwordService.compare(password, user.password))) { + const { id, roles } = user; + const roleList = roles as string[]; + return { id, username, roles: roleList }; + } + return null; + } + async login(credentials: Credentials): Promise<UserInfo> { + const { username, password } = credentials; + const user = await this.validateUser( + credentials.username, + credentials.password + ); + if (!user) { + throw new UnauthorizedException("The passed credentials are incorrect"); + } + const accessToken = await this.tokenService.createToken({ + id: user.id, + username, + password, + }); + return { + accessToken, + ...user, + }; + } +} diff --git a/apps/order-service/src/auth/base/token.service.base.ts b/apps/order-service/src/auth/base/token.service.base.ts new file mode 100644 index 0000000..f25fdb5 --- /dev/null +++ b/apps/order-service/src/auth/base/token.service.base.ts @@ -0,0 +1,25 @@ +/* eslint-disable import/no-unresolved */ +import { Injectable } from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import { INVALID_PASSWORD_ERROR, INVALID_USERNAME_ERROR } from "../constants"; +import { ITokenService, ITokenPayload } from "../ITokenService"; +/** + * TokenServiceBase is a jwt bearer implementation of ITokenService + */ +@Injectable() +export class TokenServiceBase implements ITokenService { + constructor(protected readonly jwtService: JwtService) {} + /** + * + * @object { id: String, username: String, password: String} + * @returns a jwt token sign with the username and user id + */ + createToken({ id, username, password }: ITokenPayload): Promise<string> { + if (!username) return Promise.reject(INVALID_USERNAME_ERROR); + if (!password) return Promise.reject(INVALID_PASSWORD_ERROR); + return this.jwtService.signAsync({ + sub: id, + username, + }); + } +} diff --git a/apps/order-service/src/auth/constants.ts b/apps/order-service/src/auth/constants.ts new file mode 100644 index 0000000..59f9f7d --- /dev/null +++ b/apps/order-service/src/auth/constants.ts @@ -0,0 +1,2 @@ +export const INVALID_USERNAME_ERROR = "Invalid username"; +export const INVALID_PASSWORD_ERROR = "Invalid password"; diff --git a/apps/order-service/src/auth/defaultAuth.guard.ts b/apps/order-service/src/auth/defaultAuth.guard.ts new file mode 100644 index 0000000..33a530c --- /dev/null +++ b/apps/order-service/src/auth/defaultAuth.guard.ts @@ -0,0 +1,27 @@ +import { Observable } from "rxjs"; +import { ExecutionContext, Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { IS_PUBLIC_KEY } from "../decorators/public.decorator"; +import { JwtAuthGuard } from "./jwt/jwtAuth.guard"; + +@Injectable() +export class DefaultAuthGuard extends JwtAuthGuard { + constructor(private readonly reflector: Reflector) { + super(); + } + + canActivate( + context: ExecutionContext + ): boolean | Promise<boolean> | Observable<any> { + const isPublic = this.reflector.get<boolean>( + IS_PUBLIC_KEY, + context.getHandler() + ); + + if (isPublic) { + return true; + } + + return super.canActivate(context); + } +} diff --git a/apps/order-service/src/auth/gqlAC.guard.ts b/apps/order-service/src/auth/gqlAC.guard.ts new file mode 100644 index 0000000..dacac55 --- /dev/null +++ b/apps/order-service/src/auth/gqlAC.guard.ts @@ -0,0 +1,11 @@ +import { ExecutionContext } from "@nestjs/common"; +import { GqlExecutionContext } from "@nestjs/graphql"; +import { ACGuard } from "nest-access-control"; + +export class GqlACGuard<User extends any = any> extends ACGuard<User> { + async getUser(context: ExecutionContext): Promise<User> { + const ctx = GqlExecutionContext.create(context); + const request = ctx.getContext<{ req: { user: User } }>().req; + return request.user; + } +} diff --git a/apps/order-service/src/auth/gqlDefaultAuth.guard.ts b/apps/order-service/src/auth/gqlDefaultAuth.guard.ts new file mode 100644 index 0000000..abebd93 --- /dev/null +++ b/apps/order-service/src/auth/gqlDefaultAuth.guard.ts @@ -0,0 +1,12 @@ +import { ExecutionContext } from "@nestjs/common"; +import { GqlExecutionContext } from "@nestjs/graphql"; +import type { Request } from "express"; +import { DefaultAuthGuard } from "./defaultAuth.guard"; + +export class GqlDefaultAuthGuard extends DefaultAuthGuard { + // This method is required for the interface - do not delete it. + getRequest<Request>(context: ExecutionContext): Request { + const ctx = GqlExecutionContext.create(context); + return ctx.getContext<{ req: Request }>().req; + } +} diff --git a/apps/order-service/src/auth/gqlUserRoles.decorator.ts b/apps/order-service/src/auth/gqlUserRoles.decorator.ts new file mode 100644 index 0000000..5ea256b --- /dev/null +++ b/apps/order-service/src/auth/gqlUserRoles.decorator.ts @@ -0,0 +1,19 @@ +import { createParamDecorator, ExecutionContext } from "@nestjs/common"; +import { GqlExecutionContext } from "@nestjs/graphql"; + +/** + * Access the user roles from the request object i.e `req.user.roles`. + * + * You can pass an optional property key to the decorator to get it from the user object + * e.g `@UserRoles('permissions')` will return the `req.user.permissions` instead. + */ +export const UserRoles = createParamDecorator( + (data: string, context: ExecutionContext) => { + const ctx = GqlExecutionContext.create(context); + const request = ctx.getContext<{ req: { user: any } }>().req; + if (!request.user) { + return null; + } + return data ? request.user[data] : request.user.roles; + } +); diff --git a/apps/order-service/src/auth/jwt/base/jwt.strategy.base.ts b/apps/order-service/src/auth/jwt/base/jwt.strategy.base.ts new file mode 100644 index 0000000..26da6a7 --- /dev/null +++ b/apps/order-service/src/auth/jwt/base/jwt.strategy.base.ts @@ -0,0 +1,40 @@ +import { UnauthorizedException } from "@nestjs/common"; +import { PassportStrategy } from "@nestjs/passport"; +import { ExtractJwt, Strategy } from "passport-jwt"; +import { IAuthStrategy } from "../../IAuthStrategy"; +import { UserInfo } from "../../UserInfo"; +import { UserService } from "../../../user/user.service"; + +export class JwtStrategyBase + extends PassportStrategy(Strategy) + implements IAuthStrategy +{ + constructor( + protected readonly secretOrKey: string, + protected readonly userService: UserService + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey, + }); + } + + async validate(payload: UserInfo): Promise<UserInfo> { + const { username } = payload; + const user = await this.userService.user({ + where: { username }, + }); + if (!user) { + throw new UnauthorizedException(); + } + if ( + !Array.isArray(user.roles) || + typeof user.roles !== "object" || + user.roles === null + ) { + throw new Error("User roles is not a valid value"); + } + return { ...user, roles: user.roles as string[] }; + } +} diff --git a/apps/order-service/src/auth/jwt/jwt.strategy.ts b/apps/order-service/src/auth/jwt/jwt.strategy.ts new file mode 100644 index 0000000..13395e5 --- /dev/null +++ b/apps/order-service/src/auth/jwt/jwt.strategy.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { JWT_SECRET_KEY_PROVIDER_NAME } from "../../constants"; +import { JwtStrategyBase } from "./base/jwt.strategy.base"; +import { UserService } from "../../user/user.service"; + +@Injectable() +export class JwtStrategy extends JwtStrategyBase { + constructor( + @Inject(JWT_SECRET_KEY_PROVIDER_NAME) secretOrKey: string, + protected readonly userService: UserService + ) { + super(secretOrKey, userService); + } +} diff --git a/apps/order-service/src/auth/jwt/jwtAuth.guard.ts b/apps/order-service/src/auth/jwt/jwtAuth.guard.ts new file mode 100644 index 0000000..f0c5570 --- /dev/null +++ b/apps/order-service/src/auth/jwt/jwtAuth.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from "@nestjs/passport"; + +export class JwtAuthGuard extends AuthGuard("jwt") {} diff --git a/apps/order-service/src/auth/jwt/jwtSecretFactory.ts b/apps/order-service/src/auth/jwt/jwtSecretFactory.ts new file mode 100644 index 0000000..8ca4d74 --- /dev/null +++ b/apps/order-service/src/auth/jwt/jwtSecretFactory.ts @@ -0,0 +1,19 @@ +import { JWT_SECRET_KEY_PROVIDER_NAME } from "../../constants"; +import { SecretsManagerService } from "../../providers/secrets/secretsManager.service"; +import { EnumSecretsNameKey } from "../../providers/secrets/secretsNameKey.enum"; + +export const jwtSecretFactory = { + provide: JWT_SECRET_KEY_PROVIDER_NAME, + useFactory: async ( + secretsService: SecretsManagerService + ): Promise<string> => { + const secret = await secretsService.getSecret<string>( + EnumSecretsNameKey.JwtSecretKey + ); + if (secret) { + return secret; + } + throw new Error("jwtSecretFactory missing secret"); + }, + inject: [SecretsManagerService], +}; diff --git a/apps/order-service/src/auth/password.service.spec.ts b/apps/order-service/src/auth/password.service.spec.ts new file mode 100644 index 0000000..309c8c0 --- /dev/null +++ b/apps/order-service/src/auth/password.service.spec.ts @@ -0,0 +1,69 @@ +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import { PasswordService } from "./password.service"; + +const EXAMPLE_PASSWORD = "examplePassword"; +const EXAMPLE_HASHED_PASSWORD = "exampleHashedPassword"; + +const EXAMPLE_SALT_OR_ROUNDS = 1; + +const configServiceGetMock = jest.fn(() => { + return EXAMPLE_SALT_OR_ROUNDS; +}); + +jest.mock("bcrypt", () => ({ + hash: jest.fn(), + compare: jest.fn(), +})); + +const { hash, compare } = jest.requireMock("bcrypt"); + +hash.mockImplementation(async () => EXAMPLE_HASHED_PASSWORD); + +compare.mockImplementation(async () => true); + +describe("PasswordService", () => { + let service: PasswordService; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PasswordService, + { + provide: ConfigService, + useClass: jest.fn(() => ({ + get: configServiceGetMock, + })), + }, + ], + imports: [], + }).compile(); + + service = module.get<PasswordService>(PasswordService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + it("should have salt defined", () => { + expect(service.salt).toEqual(EXAMPLE_SALT_OR_ROUNDS); + }); + + it("should compare a password", async () => { + const args = { + password: EXAMPLE_PASSWORD, + hashedPassword: EXAMPLE_HASHED_PASSWORD, + }; + await expect( + service.compare(args.password, args.hashedPassword) + ).resolves.toEqual(true); + }); + + it("should hash a password", async () => { + await expect(service.hash(EXAMPLE_PASSWORD)).resolves.toEqual( + EXAMPLE_HASHED_PASSWORD + ); + }); +}); diff --git a/apps/order-service/src/auth/password.service.ts b/apps/order-service/src/auth/password.service.ts new file mode 100644 index 0000000..377b64b --- /dev/null +++ b/apps/order-service/src/auth/password.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from "@nestjs/common"; +import { hash, compare } from "bcrypt"; +import { ConfigService } from "@nestjs/config"; + +/** Salt or number of rounds to generate a salt */ +export type Salt = string | number; + +const BCRYPT_SALT_VAR = "BCRYPT_SALT"; +const UNDEFINED_SALT_OR_ROUNDS_ERROR = `${BCRYPT_SALT_VAR} is not defined`; +const SALT_OR_ROUNDS_TYPE_ERROR = `${BCRYPT_SALT_VAR} must be a positive integer or text`; + +@Injectable() +export class PasswordService { + /** + * the salt to be used to hash the password. if specified as a number then a + * salt will be generated with the specified number of rounds and used + */ + salt: Salt; + + constructor(private configService: ConfigService) { + const saltOrRounds = this.configService.get(BCRYPT_SALT_VAR); + this.salt = parseSalt(saltOrRounds); + } + + /** + * + * @param password the password to be encrypted. + * @param encrypted the encrypted password to be compared against. + * @returns whether the password match the encrypted password + */ + compare(password: string, encrypted: string): Promise<boolean> { + return compare(password, encrypted); + } + + /** + * @param password the password to be encrypted + * @return encrypted password + */ + hash(password: string): Promise<string> { + return hash(password, this.salt); + } +} + +/** + * Parses a salt environment variable value. + * If a number string value is given tries to parse it as a number of rounds to generate a salt + * @param value salt environment variable value + * @returns salt or number of rounds to generate a salt + */ +export function parseSalt(value: string | undefined): Salt { + if (value === undefined) { + throw new Error(UNDEFINED_SALT_OR_ROUNDS_ERROR); + } + + const rounds = Number(value); + + if (Number.isNaN(rounds)) { + return value; + } + if (!Number.isInteger(rounds) || rounds < 0) { + throw new Error(SALT_OR_ROUNDS_TYPE_ERROR); + } + return rounds; +} diff --git a/apps/order-service/src/auth/token.service.ts b/apps/order-service/src/auth/token.service.ts new file mode 100644 index 0000000..2ee079c --- /dev/null +++ b/apps/order-service/src/auth/token.service.ts @@ -0,0 +1,5 @@ +import { ITokenService } from "./ITokenService"; + +import { TokenServiceBase } from "./base/token.service.base"; + +export class TokenService extends TokenServiceBase implements ITokenService {} diff --git a/apps/order-service/src/auth/userData.decorator.ts b/apps/order-service/src/auth/userData.decorator.ts new file mode 100644 index 0000000..6a40ad1 --- /dev/null +++ b/apps/order-service/src/auth/userData.decorator.ts @@ -0,0 +1,30 @@ +import { createParamDecorator, ExecutionContext } from "@nestjs/common"; +import { GqlContextType, GqlExecutionContext } from "@nestjs/graphql"; +import { User } from "@prisma/client"; + +/** + * Access the user data from the request object i.e `req.user`. + */ +function userFactory(ctx: ExecutionContext): User { + const contextType = ctx.getType(); + if (contextType === "http") { + // do something that is only important in the context of regular HTTP requests (REST) + const { user } = ctx.switchToHttp().getRequest(); + return user; + } else if (contextType === "rpc") { + // do something that is only important in the context of Microservice requests + throw new Error("Rpc context is not implemented yet"); + } else if (contextType === "ws") { + // do something that is only important in the context of Websockets requests + throw new Error("Websockets context is not implemented yet"); + } else if (ctx.getType<GqlContextType>() === "graphql") { + // do something that is only important in the context of GraphQL requests + const gqlExecutionContext = GqlExecutionContext.create(ctx); + return gqlExecutionContext.getContext().req.user; + } + throw new Error("Invalid context"); +} + +export const UserData = createParamDecorator<undefined, ExecutionContext, User>( + (data, ctx: ExecutionContext) => userFactory(ctx) +); diff --git a/apps/order-service/src/connectMicroservices.ts b/apps/order-service/src/connectMicroservices.ts new file mode 100644 index 0000000..c1360d0 --- /dev/null +++ b/apps/order-service/src/connectMicroservices.ts @@ -0,0 +1,9 @@ +import { INestApplication } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { generateKafkaClientOptions } from "./kafka/generateKafkaClientOptions"; +import { MicroserviceOptions } from "@nestjs/microservices"; + +export async function connectMicroservices(app: INestApplication) { + const configService = app.get(ConfigService); + app.connectMicroservice<MicroserviceOptions>(generateKafkaClientOptions(configService)); +} diff --git a/apps/order-service/src/constants.ts b/apps/order-service/src/constants.ts new file mode 100644 index 0000000..e131049 --- /dev/null +++ b/apps/order-service/src/constants.ts @@ -0,0 +1,2 @@ +export const JWT_SECRET_KEY_PROVIDER_NAME = "JWT_SECRET_KEY"; +export const JWT_EXPIRATION = "JWT_EXPIRATION"; diff --git a/apps/order-service/src/customer/base/CreateCustomerArgs.ts b/apps/order-service/src/customer/base/CreateCustomerArgs.ts new file mode 100644 index 0000000..893eb27 --- /dev/null +++ b/apps/order-service/src/customer/base/CreateCustomerArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerCreateInput } from "./CustomerCreateInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CreateCustomerArgs { + @ApiProperty({ + required: true, + type: () => CustomerCreateInput, + }) + @ValidateNested() + @Type(() => CustomerCreateInput) + @Field(() => CustomerCreateInput, { nullable: false }) + data!: CustomerCreateInput; +} + +export { CreateCustomerArgs as CreateCustomerArgs }; diff --git a/apps/order-service/src/customer/base/Customer.ts b/apps/order-service/src/customer/base/Customer.ts new file mode 100644 index 0000000..9b8a699 --- /dev/null +++ b/apps/order-service/src/customer/base/Customer.ts @@ -0,0 +1,117 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { Address } from "../../address/base/Address"; +import { + ValidateNested, + IsOptional, + IsDate, + IsString, + MaxLength, +} from "class-validator"; +import { Type } from "class-transformer"; +import { Order } from "../../order/base/Order"; + +@ObjectType() +class Customer { + @ApiProperty({ + required: false, + type: () => Address, + }) + @ValidateNested() + @Type(() => Address) + @IsOptional() + address?: Address | null; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + createdAt!: Date; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + email!: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + firstName!: string | null; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + lastName!: string | null; + + @ApiProperty({ + required: false, + type: () => Order, + }) + @ValidateNested() + @Type(() => Order) + @IsOptional() + orders?: Order | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + phone!: string | null; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + updatedAt!: Date; +} + +export { Customer as Customer }; diff --git a/apps/order-service/src/customer/base/CustomerCountArgs.ts b/apps/order-service/src/customer/base/CustomerCountArgs.ts new file mode 100644 index 0000000..9246fde --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerCountArgs.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereInput } from "./CustomerWhereInput"; +import { Type } from "class-transformer"; + +@ArgsType() +class CustomerCountArgs { + @ApiProperty({ + required: false, + type: () => CustomerWhereInput, + }) + @Field(() => CustomerWhereInput, { nullable: true }) + @Type(() => CustomerWhereInput) + where?: CustomerWhereInput; +} + +export { CustomerCountArgs as CustomerCountArgs }; diff --git a/apps/order-service/src/customer/base/CustomerCreateInput.ts b/apps/order-service/src/customer/base/CustomerCreateInput.ts new file mode 100644 index 0000000..718c778 --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerCreateInput.ts @@ -0,0 +1,98 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereUniqueInput } from "../../address/base/AddressWhereUniqueInput"; +import { + ValidateNested, + IsOptional, + IsString, + MaxLength, +} from "class-validator"; +import { Type } from "class-transformer"; +import { OrderWhereUniqueInput } from "../../order/base/OrderWhereUniqueInput"; + +@InputType() +class CustomerCreateInput { + @ApiProperty({ + required: false, + type: () => AddressWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AddressWhereUniqueInput) + @IsOptional() + @Field(() => AddressWhereUniqueInput, { + nullable: true, + }) + address?: AddressWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + email?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + firstName?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + lastName?: string | null; + + @ApiProperty({ + required: false, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @IsOptional() + @Field(() => OrderWhereUniqueInput, { + nullable: true, + }) + orders?: OrderWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + phone?: string | null; +} + +export { CustomerCreateInput as CustomerCreateInput }; diff --git a/apps/order-service/src/customer/base/CustomerFindManyArgs.ts b/apps/order-service/src/customer/base/CustomerFindManyArgs.ts new file mode 100644 index 0000000..dfecb60 --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerFindManyArgs.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereInput } from "./CustomerWhereInput"; +import { IsOptional, ValidateNested, IsInt } from "class-validator"; +import { Type } from "class-transformer"; +import { CustomerOrderByInput } from "./CustomerOrderByInput"; + +@ArgsType() +class CustomerFindManyArgs { + @ApiProperty({ + required: false, + type: () => CustomerWhereInput, + }) + @IsOptional() + @ValidateNested() + @Field(() => CustomerWhereInput, { nullable: true }) + @Type(() => CustomerWhereInput) + where?: CustomerWhereInput; + + @ApiProperty({ + required: false, + type: [CustomerOrderByInput], + }) + @IsOptional() + @ValidateNested({ each: true }) + @Field(() => [CustomerOrderByInput], { nullable: true }) + @Type(() => CustomerOrderByInput) + orderBy?: Array<CustomerOrderByInput>; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + skip?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + take?: number; +} + +export { CustomerFindManyArgs as CustomerFindManyArgs }; diff --git a/apps/order-service/src/customer/base/CustomerFindUniqueArgs.ts b/apps/order-service/src/customer/base/CustomerFindUniqueArgs.ts new file mode 100644 index 0000000..958977a --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerFindUniqueArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereUniqueInput } from "./CustomerWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CustomerFindUniqueArgs { + @ApiProperty({ + required: true, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @Field(() => CustomerWhereUniqueInput, { nullable: false }) + where!: CustomerWhereUniqueInput; +} + +export { CustomerFindUniqueArgs as CustomerFindUniqueArgs }; diff --git a/apps/order-service/src/customer/base/CustomerListRelationFilter.ts b/apps/order-service/src/customer/base/CustomerListRelationFilter.ts new file mode 100644 index 0000000..5d73d88 --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerListRelationFilter.ts @@ -0,0 +1,56 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereInput } from "./CustomerWhereInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class CustomerListRelationFilter { + @ApiProperty({ + required: false, + type: () => CustomerWhereInput, + }) + @ValidateNested() + @Type(() => CustomerWhereInput) + @IsOptional() + @Field(() => CustomerWhereInput, { + nullable: true, + }) + every?: CustomerWhereInput; + + @ApiProperty({ + required: false, + type: () => CustomerWhereInput, + }) + @ValidateNested() + @Type(() => CustomerWhereInput) + @IsOptional() + @Field(() => CustomerWhereInput, { + nullable: true, + }) + some?: CustomerWhereInput; + + @ApiProperty({ + required: false, + type: () => CustomerWhereInput, + }) + @ValidateNested() + @Type(() => CustomerWhereInput) + @IsOptional() + @Field(() => CustomerWhereInput, { + nullable: true, + }) + none?: CustomerWhereInput; +} +export { CustomerListRelationFilter as CustomerListRelationFilter }; diff --git a/apps/order-service/src/customer/base/CustomerOrderByInput.ts b/apps/order-service/src/customer/base/CustomerOrderByInput.ts new file mode 100644 index 0000000..f3cce67 --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerOrderByInput.ts @@ -0,0 +1,122 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsEnum } from "class-validator"; +import { SortOrder } from "../../util/SortOrder"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +class CustomerOrderByInput { + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + addressId?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + createdAt?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + email?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + firstName?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + id?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + lastName?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + ordersId?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + phone?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + updatedAt?: SortOrder; +} + +export { CustomerOrderByInput as CustomerOrderByInput }; diff --git a/apps/order-service/src/customer/base/CustomerUpdateInput.ts b/apps/order-service/src/customer/base/CustomerUpdateInput.ts new file mode 100644 index 0000000..52e2200 --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerUpdateInput.ts @@ -0,0 +1,98 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereUniqueInput } from "../../address/base/AddressWhereUniqueInput"; +import { + ValidateNested, + IsOptional, + IsString, + MaxLength, +} from "class-validator"; +import { Type } from "class-transformer"; +import { OrderWhereUniqueInput } from "../../order/base/OrderWhereUniqueInput"; + +@InputType() +class CustomerUpdateInput { + @ApiProperty({ + required: false, + type: () => AddressWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AddressWhereUniqueInput) + @IsOptional() + @Field(() => AddressWhereUniqueInput, { + nullable: true, + }) + address?: AddressWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + email?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + firstName?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + lastName?: string | null; + + @ApiProperty({ + required: false, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @IsOptional() + @Field(() => OrderWhereUniqueInput, { + nullable: true, + }) + orders?: OrderWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + phone?: string | null; +} + +export { CustomerUpdateInput as CustomerUpdateInput }; diff --git a/apps/order-service/src/customer/base/CustomerWhereInput.ts b/apps/order-service/src/customer/base/CustomerWhereInput.ts new file mode 100644 index 0000000..f80bbb8 --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerWhereInput.ts @@ -0,0 +1,103 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { AddressWhereUniqueInput } from "../../address/base/AddressWhereUniqueInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { StringFilter } from "../../util/StringFilter"; +import { OrderWhereUniqueInput } from "../../order/base/OrderWhereUniqueInput"; + +@InputType() +class CustomerWhereInput { + @ApiProperty({ + required: false, + type: () => AddressWhereUniqueInput, + }) + @ValidateNested() + @Type(() => AddressWhereUniqueInput) + @IsOptional() + @Field(() => AddressWhereUniqueInput, { + nullable: true, + }) + address?: AddressWhereUniqueInput; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + email?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + firstName?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + id?: StringFilter; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + lastName?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @IsOptional() + @Field(() => OrderWhereUniqueInput, { + nullable: true, + }) + orders?: OrderWhereUniqueInput; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + phone?: StringNullableFilter; +} + +export { CustomerWhereInput as CustomerWhereInput }; diff --git a/apps/order-service/src/customer/base/CustomerWhereUniqueInput.ts b/apps/order-service/src/customer/base/CustomerWhereUniqueInput.ts new file mode 100644 index 0000000..02c3d87 --- /dev/null +++ b/apps/order-service/src/customer/base/CustomerWhereUniqueInput.ts @@ -0,0 +1,27 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +@InputType() +class CustomerWhereUniqueInput { + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; +} + +export { CustomerWhereUniqueInput as CustomerWhereUniqueInput }; diff --git a/apps/order-service/src/customer/base/DeleteCustomerArgs.ts b/apps/order-service/src/customer/base/DeleteCustomerArgs.ts new file mode 100644 index 0000000..c5586b9 --- /dev/null +++ b/apps/order-service/src/customer/base/DeleteCustomerArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereUniqueInput } from "./CustomerWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class DeleteCustomerArgs { + @ApiProperty({ + required: true, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @Field(() => CustomerWhereUniqueInput, { nullable: false }) + where!: CustomerWhereUniqueInput; +} + +export { DeleteCustomerArgs as DeleteCustomerArgs }; diff --git a/apps/order-service/src/customer/base/UpdateCustomerArgs.ts b/apps/order-service/src/customer/base/UpdateCustomerArgs.ts new file mode 100644 index 0000000..d38e938 --- /dev/null +++ b/apps/order-service/src/customer/base/UpdateCustomerArgs.ts @@ -0,0 +1,40 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereUniqueInput } from "./CustomerWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { CustomerUpdateInput } from "./CustomerUpdateInput"; + +@ArgsType() +class UpdateCustomerArgs { + @ApiProperty({ + required: true, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @Field(() => CustomerWhereUniqueInput, { nullable: false }) + where!: CustomerWhereUniqueInput; + + @ApiProperty({ + required: true, + type: () => CustomerUpdateInput, + }) + @ValidateNested() + @Type(() => CustomerUpdateInput) + @Field(() => CustomerUpdateInput, { nullable: false }) + data!: CustomerUpdateInput; +} + +export { UpdateCustomerArgs as UpdateCustomerArgs }; diff --git a/apps/order-service/src/customer/base/customer.controller.base.spec.ts b/apps/order-service/src/customer/base/customer.controller.base.spec.ts new file mode 100644 index 0000000..e5bb2ad --- /dev/null +++ b/apps/order-service/src/customer/base/customer.controller.base.spec.ts @@ -0,0 +1,206 @@ +import { Test } from "@nestjs/testing"; +import { + INestApplication, + HttpStatus, + ExecutionContext, + CallHandler, +} from "@nestjs/common"; +import request from "supertest"; +import { ACGuard } from "nest-access-control"; +import { DefaultAuthGuard } from "../../auth/defaultAuth.guard"; +import { ACLModule } from "../../auth/acl.module"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { map } from "rxjs"; +import { CustomerController } from "../customer.controller"; +import { CustomerService } from "../customer.service"; + +const nonExistingId = "nonExistingId"; +const existingId = "existingId"; +const CREATE_INPUT = { + createdAt: new Date(), + email: "exampleEmail", + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + phone: "examplePhone", + updatedAt: new Date(), +}; +const CREATE_RESULT = { + createdAt: new Date(), + email: "exampleEmail", + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + phone: "examplePhone", + updatedAt: new Date(), +}; +const FIND_MANY_RESULT = [ + { + createdAt: new Date(), + email: "exampleEmail", + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + phone: "examplePhone", + updatedAt: new Date(), + }, +]; +const FIND_ONE_RESULT = { + createdAt: new Date(), + email: "exampleEmail", + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + phone: "examplePhone", + updatedAt: new Date(), +}; + +const service = { + createCustomer() { + return CREATE_RESULT; + }, + customers: () => FIND_MANY_RESULT, + customer: ({ where }: { where: { id: string } }) => { + switch (where.id) { + case existingId: + return FIND_ONE_RESULT; + case nonExistingId: + return null; + } + }, +}; + +const basicAuthGuard = { + canActivate: (context: ExecutionContext) => { + const argumentHost = context.switchToHttp(); + const request = argumentHost.getRequest(); + request.user = { + roles: ["user"], + }; + return true; + }, +}; + +const acGuard = { + canActivate: () => { + return true; + }, +}; + +const aclFilterResponseInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle().pipe( + map((data) => { + return data; + }) + ); + }, +}; +const aclValidateRequestInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle(); + }, +}; + +describe("Customer", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: CustomerService, + useValue: service, + }, + ], + controllers: [CustomerController], + imports: [ACLModule], + }) + .overrideGuard(DefaultAuthGuard) + .useValue(basicAuthGuard) + .overrideGuard(ACGuard) + .useValue(acGuard) + .overrideInterceptor(AclFilterResponseInterceptor) + .useValue(aclFilterResponseInterceptor) + .overrideInterceptor(AclValidateRequestInterceptor) + .useValue(aclValidateRequestInterceptor) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + test("POST /customers", async () => { + await request(app.getHttpServer()) + .post("/customers") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }); + }); + + test("GET /customers", async () => { + await request(app.getHttpServer()) + .get("/customers") + .expect(HttpStatus.OK) + .expect([ + { + ...FIND_MANY_RESULT[0], + createdAt: FIND_MANY_RESULT[0].createdAt.toISOString(), + updatedAt: FIND_MANY_RESULT[0].updatedAt.toISOString(), + }, + ]); + }); + + test("GET /customers/:id non existing", async () => { + await request(app.getHttpServer()) + .get(`${"/customers"}/${nonExistingId}`) + .expect(HttpStatus.NOT_FOUND) + .expect({ + statusCode: HttpStatus.NOT_FOUND, + message: `No resource was found for {"${"id"}":"${nonExistingId}"}`, + error: "Not Found", + }); + }); + + test("GET /customers/:id existing", async () => { + await request(app.getHttpServer()) + .get(`${"/customers"}/${existingId}`) + .expect(HttpStatus.OK) + .expect({ + ...FIND_ONE_RESULT, + createdAt: FIND_ONE_RESULT.createdAt.toISOString(), + updatedAt: FIND_ONE_RESULT.updatedAt.toISOString(), + }); + }); + + test("POST /customers existing resource", async () => { + const agent = request(app.getHttpServer()); + await agent + .post("/customers") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }) + .then(function () { + agent + .post("/customers") + .send(CREATE_INPUT) + .expect(HttpStatus.CONFLICT) + .expect({ + statusCode: HttpStatus.CONFLICT, + }); + }); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/apps/order-service/src/customer/base/customer.controller.base.ts b/apps/order-service/src/customer/base/customer.controller.base.ts new file mode 100644 index 0000000..d7728e6 --- /dev/null +++ b/apps/order-service/src/customer/base/customer.controller.base.ts @@ -0,0 +1,297 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { isRecordNotFoundError } from "../../prisma.util"; +import * as errors from "../../errors"; +import { Request } from "express"; +import { plainToClass } from "class-transformer"; +import { ApiNestedQuery } from "../../decorators/api-nested-query.decorator"; +import * as nestAccessControl from "nest-access-control"; +import * as defaultAuthGuard from "../../auth/defaultAuth.guard"; +import { CustomerService } from "../customer.service"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { CustomerCreateInput } from "./CustomerCreateInput"; +import { Customer } from "./Customer"; +import { CustomerFindManyArgs } from "./CustomerFindManyArgs"; +import { CustomerWhereUniqueInput } from "./CustomerWhereUniqueInput"; +import { CustomerUpdateInput } from "./CustomerUpdateInput"; + +@swagger.ApiBearerAuth() +@common.UseGuards(defaultAuthGuard.DefaultAuthGuard, nestAccessControl.ACGuard) +export class CustomerControllerBase { + constructor( + protected readonly service: CustomerService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Post() + @swagger.ApiCreatedResponse({ type: Customer }) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "create", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async createCustomer( + @common.Body() data: CustomerCreateInput + ): Promise<Customer> { + return await this.service.createCustomer({ + data: { + ...data, + + address: data.address + ? { + connect: data.address, + } + : undefined, + + orders: data.orders + ? { + connect: data.orders, + } + : undefined, + }, + select: { + address: { + select: { + id: true, + }, + }, + + createdAt: true, + email: true, + firstName: true, + id: true, + lastName: true, + + orders: { + select: { + id: true, + }, + }, + + phone: true, + updatedAt: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get() + @swagger.ApiOkResponse({ type: [Customer] }) + @ApiNestedQuery(CustomerFindManyArgs) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "read", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async customers(@common.Req() request: Request): Promise<Customer[]> { + const args = plainToClass(CustomerFindManyArgs, request.query); + return this.service.customers({ + ...args, + select: { + address: { + select: { + id: true, + }, + }, + + createdAt: true, + email: true, + firstName: true, + id: true, + lastName: true, + + orders: { + select: { + id: true, + }, + }, + + phone: true, + updatedAt: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get("/:id") + @swagger.ApiOkResponse({ type: Customer }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "read", + possession: "own", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async customer( + @common.Param() params: CustomerWhereUniqueInput + ): Promise<Customer | null> { + const result = await this.service.customer({ + where: params, + select: { + address: { + select: { + id: true, + }, + }, + + createdAt: true, + email: true, + firstName: true, + id: true, + lastName: true, + + orders: { + select: { + id: true, + }, + }, + + phone: true, + updatedAt: true, + }, + }); + if (result === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Patch("/:id") + @swagger.ApiOkResponse({ type: Customer }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "update", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async updateCustomer( + @common.Param() params: CustomerWhereUniqueInput, + @common.Body() data: CustomerUpdateInput + ): Promise<Customer | null> { + try { + return await this.service.updateCustomer({ + where: params, + data: { + ...data, + + address: data.address + ? { + connect: data.address, + } + : undefined, + + orders: data.orders + ? { + connect: data.orders, + } + : undefined, + }, + select: { + address: { + select: { + id: true, + }, + }, + + createdAt: true, + email: true, + firstName: true, + id: true, + lastName: true, + + orders: { + select: { + id: true, + }, + }, + + phone: true, + updatedAt: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Delete("/:id") + @swagger.ApiOkResponse({ type: Customer }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "delete", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async deleteCustomer( + @common.Param() params: CustomerWhereUniqueInput + ): Promise<Customer | null> { + try { + return await this.service.deleteCustomer({ + where: params, + select: { + address: { + select: { + id: true, + }, + }, + + createdAt: true, + email: true, + firstName: true, + id: true, + lastName: true, + + orders: { + select: { + id: true, + }, + }, + + phone: true, + updatedAt: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } +} diff --git a/apps/order-service/src/customer/base/customer.module.base.ts b/apps/order-service/src/customer/base/customer.module.base.ts new file mode 100644 index 0000000..d1cd308 --- /dev/null +++ b/apps/order-service/src/customer/base/customer.module.base.ts @@ -0,0 +1,18 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { Module } from "@nestjs/common"; +import { ACLModule } from "../../auth/acl.module"; +@Module({ + imports: [ACLModule], + exports: [ACLModule], +}) +export class CustomerModuleBase {} diff --git a/apps/order-service/src/customer/base/customer.resolver.base.ts b/apps/order-service/src/customer/base/customer.resolver.base.ts new file mode 100644 index 0000000..2efeed2 --- /dev/null +++ b/apps/order-service/src/customer/base/customer.resolver.base.ts @@ -0,0 +1,214 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as graphql from "@nestjs/graphql"; +import { GraphQLError } from "graphql"; +import { isRecordNotFoundError } from "../../prisma.util"; +import { MetaQueryPayload } from "../../util/MetaQueryPayload"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { Customer } from "./Customer"; +import { CustomerCountArgs } from "./CustomerCountArgs"; +import { CustomerFindManyArgs } from "./CustomerFindManyArgs"; +import { CustomerFindUniqueArgs } from "./CustomerFindUniqueArgs"; +import { CreateCustomerArgs } from "./CreateCustomerArgs"; +import { UpdateCustomerArgs } from "./UpdateCustomerArgs"; +import { DeleteCustomerArgs } from "./DeleteCustomerArgs"; +import { Address } from "../../address/base/Address"; +import { Order } from "../../order/base/Order"; +import { CustomerService } from "../customer.service"; +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Customer) +export class CustomerResolverBase { + constructor( + protected readonly service: CustomerService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + + @graphql.Query(() => MetaQueryPayload) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "read", + possession: "any", + }) + async _customersMeta( + @graphql.Args() args: CustomerCountArgs + ): Promise<MetaQueryPayload> { + const result = await this.service.count(args); + return { + count: result, + }; + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => [Customer]) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "read", + possession: "any", + }) + async customers( + @graphql.Args() args: CustomerFindManyArgs + ): Promise<Customer[]> { + return this.service.customers(args); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => Customer, { nullable: true }) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "read", + possession: "own", + }) + async customer( + @graphql.Args() args: CustomerFindUniqueArgs + ): Promise<Customer | null> { + const result = await this.service.customer(args); + if (result === null) { + return null; + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Customer) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "create", + possession: "any", + }) + async createCustomer( + @graphql.Args() args: CreateCustomerArgs + ): Promise<Customer> { + return await this.service.createCustomer({ + ...args, + data: { + ...args.data, + + address: args.data.address + ? { + connect: args.data.address, + } + : undefined, + + orders: args.data.orders + ? { + connect: args.data.orders, + } + : undefined, + }, + }); + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Customer) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "update", + possession: "any", + }) + async updateCustomer( + @graphql.Args() args: UpdateCustomerArgs + ): Promise<Customer | null> { + try { + return await this.service.updateCustomer({ + ...args, + data: { + ...args.data, + + address: args.data.address + ? { + connect: args.data.address, + } + : undefined, + + orders: args.data.orders + ? { + connect: args.data.orders, + } + : undefined, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.Mutation(() => Customer) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "delete", + possession: "any", + }) + async deleteCustomer( + @graphql.Args() args: DeleteCustomerArgs + ): Promise<Customer | null> { + try { + return await this.service.deleteCustomer(args); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.ResolveField(() => Address, { + nullable: true, + name: "address", + }) + @nestAccessControl.UseRoles({ + resource: "Address", + action: "read", + possession: "any", + }) + async getAddress( + @graphql.Parent() parent: Customer + ): Promise<Address | null> { + const result = await this.service.getAddress(parent.id); + + if (!result) { + return null; + } + return result; + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.ResolveField(() => Order, { + nullable: true, + name: "orders", + }) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "read", + possession: "any", + }) + async getOrders(@graphql.Parent() parent: Customer): Promise<Order | null> { + const result = await this.service.getOrders(parent.id); + + if (!result) { + return null; + } + return result; + } +} diff --git a/apps/order-service/src/customer/base/customer.service.base.ts b/apps/order-service/src/customer/base/customer.service.base.ts new file mode 100644 index 0000000..01558d6 --- /dev/null +++ b/apps/order-service/src/customer/base/customer.service.base.ts @@ -0,0 +1,69 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { PrismaService } from "../../prisma/prisma.service"; + +import { + Prisma, + Customer as PrismaCustomer, + Address as PrismaAddress, + Order as PrismaOrder, +} from "@prisma/client"; + +export class CustomerServiceBase { + constructor(protected readonly prisma: PrismaService) {} + + async count(args: Omit<Prisma.CustomerCountArgs, "select">): Promise<number> { + return this.prisma.customer.count(args); + } + + async customers( + args: Prisma.CustomerFindManyArgs + ): Promise<PrismaCustomer[]> { + return this.prisma.customer.findMany(args); + } + async customer( + args: Prisma.CustomerFindUniqueArgs + ): Promise<PrismaCustomer | null> { + return this.prisma.customer.findUnique(args); + } + async createCustomer( + args: Prisma.CustomerCreateArgs + ): Promise<PrismaCustomer> { + return this.prisma.customer.create(args); + } + async updateCustomer( + args: Prisma.CustomerUpdateArgs + ): Promise<PrismaCustomer> { + return this.prisma.customer.update(args); + } + async deleteCustomer( + args: Prisma.CustomerDeleteArgs + ): Promise<PrismaCustomer> { + return this.prisma.customer.delete(args); + } + + async getAddress(parentId: string): Promise<PrismaAddress | null> { + return this.prisma.customer + .findUnique({ + where: { id: parentId }, + }) + .address(); + } + + async getOrders(parentId: string): Promise<PrismaOrder | null> { + return this.prisma.customer + .findUnique({ + where: { id: parentId }, + }) + .orders(); + } +} diff --git a/apps/order-service/src/customer/customer.controller.ts b/apps/order-service/src/customer/customer.controller.ts new file mode 100644 index 0000000..1da4964 --- /dev/null +++ b/apps/order-service/src/customer/customer.controller.ts @@ -0,0 +1,17 @@ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import * as nestAccessControl from "nest-access-control"; +import { CustomerService } from "./customer.service"; +import { CustomerControllerBase } from "./base/customer.controller.base"; + +@swagger.ApiTags("customers") +@common.Controller("customers") +export class CustomerController extends CustomerControllerBase { + constructor( + protected readonly service: CustomerService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/customer/customer.module.ts b/apps/order-service/src/customer/customer.module.ts new file mode 100644 index 0000000..e6d2f1a --- /dev/null +++ b/apps/order-service/src/customer/customer.module.ts @@ -0,0 +1,14 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module"; +import { CustomerModuleBase } from "./base/customer.module.base"; +import { CustomerService } from "./customer.service"; +import { CustomerController } from "./customer.controller"; +import { CustomerResolver } from "./customer.resolver"; + +@Module({ + imports: [CustomerModuleBase, forwardRef(() => AuthModule)], + controllers: [CustomerController], + providers: [CustomerService, CustomerResolver], + exports: [CustomerService], +}) +export class CustomerModule {} diff --git a/apps/order-service/src/customer/customer.resolver.ts b/apps/order-service/src/customer/customer.resolver.ts new file mode 100644 index 0000000..1113ac5 --- /dev/null +++ b/apps/order-service/src/customer/customer.resolver.ts @@ -0,0 +1,20 @@ +import * as graphql from "@nestjs/graphql"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { CustomerResolverBase } from "./base/customer.resolver.base"; +import { Customer } from "./base/Customer"; +import { CustomerService } from "./customer.service"; + +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Customer) +export class CustomerResolver extends CustomerResolverBase { + constructor( + protected readonly service: CustomerService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/customer/customer.service.ts b/apps/order-service/src/customer/customer.service.ts new file mode 100644 index 0000000..534d665 --- /dev/null +++ b/apps/order-service/src/customer/customer.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { CustomerServiceBase } from "./base/customer.service.base"; + +@Injectable() +export class CustomerService extends CustomerServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/apps/order-service/src/decorators/api-nested-query.decorator.ts b/apps/order-service/src/decorators/api-nested-query.decorator.ts new file mode 100644 index 0000000..9fd5ba3 --- /dev/null +++ b/apps/order-service/src/decorators/api-nested-query.decorator.ts @@ -0,0 +1,80 @@ +import { applyDecorators } from "@nestjs/common"; +import { + ApiExtraModels, + ApiQuery, + ApiQueryOptions, + getSchemaPath, +} from "@nestjs/swagger"; +import "reflect-metadata"; + +const generateApiQueryObject = ( + prop: any, + propType: any, + required: boolean, + isArray: boolean +): ApiQueryOptions => { + if (propType === Number) { + return { + required, + name: prop, + style: "deepObject", + explode: true, + type: "number", + isArray, + }; + } else if (propType === String) { + return { + required, + name: prop, + style: "deepObject", + explode: true, + type: "string", + isArray, + }; + } else { + return { + required, + name: prop, + style: "deepObject", + explode: true, + type: "object", + isArray, + schema: { + $ref: getSchemaPath(propType), + }, + }; + } +}; + +// eslint-disable-next-line @typescript-eslint/ban-types,@typescript-eslint/explicit-module-boundary-types,@typescript-eslint/naming-convention +export function ApiNestedQuery(query: Function) { + const constructor = query.prototype; + const properties = Reflect.getMetadata( + "swagger/apiModelPropertiesArray", + constructor + ).map((prop: any) => prop.slice(1)); + + const decorators = properties + .map((property: any) => { + const { required, isArray } = Reflect.getMetadata( + "swagger/apiModelProperties", + constructor, + property + ); + const propertyType = Reflect.getMetadata( + "design:type", + constructor, + property + ); + const typedQuery = generateApiQueryObject( + property, + propertyType, + required, + isArray + ); + return [ApiExtraModels(propertyType), ApiQuery(typedQuery)]; + }) + .flat(); + + return applyDecorators(...decorators); +} diff --git a/apps/order-service/src/decorators/public.decorator.ts b/apps/order-service/src/decorators/public.decorator.ts new file mode 100644 index 0000000..9eab4e0 --- /dev/null +++ b/apps/order-service/src/decorators/public.decorator.ts @@ -0,0 +1,10 @@ +import { applyDecorators, SetMetadata } from "@nestjs/common"; + +export const IS_PUBLIC_KEY = "isPublic"; + +const PublicAuthMiddleware = SetMetadata(IS_PUBLIC_KEY, true); +const PublicAuthSwagger = SetMetadata("swagger/apiSecurity", ["isPublic"]); + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const Public = () => + applyDecorators(PublicAuthMiddleware, PublicAuthSwagger); diff --git a/apps/order-service/src/errors.ts b/apps/order-service/src/errors.ts new file mode 100644 index 0000000..bd1aa6d --- /dev/null +++ b/apps/order-service/src/errors.ts @@ -0,0 +1,16 @@ +import * as common from "@nestjs/common"; +import { ApiProperty } from "@nestjs/swagger"; + +export class ForbiddenException extends common.ForbiddenException { + @ApiProperty() + statusCode!: number; + @ApiProperty() + message!: string; +} + +export class NotFoundException extends common.NotFoundException { + @ApiProperty() + statusCode!: number; + @ApiProperty() + message!: string; +} diff --git a/apps/order-service/src/filters/HttpExceptions.filter.ts b/apps/order-service/src/filters/HttpExceptions.filter.ts new file mode 100644 index 0000000..f5eda8e --- /dev/null +++ b/apps/order-service/src/filters/HttpExceptions.filter.ts @@ -0,0 +1,89 @@ +import { + ArgumentsHost, + Catch, + HttpException, + HttpServer, + HttpStatus, +} from "@nestjs/common"; +import { BaseExceptionFilter } from "@nestjs/core"; +import { Prisma } from "@prisma/client"; +import { Response } from "express"; + +export type ErrorCodesStatusMapping = { + [key: string]: number; +}; + +/** + * {@link PrismaClientExceptionFilter} handling {@link Prisma.PrismaClientKnownRequestError} exceptions. + */ +@Catch(Prisma?.PrismaClientKnownRequestError) +export class HttpExceptionFilter extends BaseExceptionFilter { + /** + * default error codes mapping + * + * Error codes definition for Prisma Client (Query Engine) + * @see https://www.prisma.io/docs/reference/api-reference/error-reference#prisma-client-query-engine + */ + private errorCodesStatusMapping: ErrorCodesStatusMapping = { + P2000: HttpStatus.BAD_REQUEST, + P2002: HttpStatus.CONFLICT, + P2025: HttpStatus.NOT_FOUND, + }; + + /** + * @param applicationRef + */ + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(applicationRef?: HttpServer) { + super(applicationRef); + } + + /** + * @param exception + * @param host + * @returns + */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) { + const statusCode = this.errorCodesStatusMapping[exception.code]; + let message; + if (host.getType() === "http") { + // for http requests (REST) + // Todo : Add all other exception types and also add mapping + const ctx = host.switchToHttp(); + const response = ctx.getResponse<Response>(); + if (exception.code === "P2002") { + // Handling Unique Key Constraint Violation Error + const fields = (exception.meta as { target: string[] }).target; + message = `Another record with the requested (${fields.join( + ", " + )}) already exists`; + } else { + message = + `[${exception.code}]: ` + + this.exceptionShortMessage(exception.message); + } + if (!Object.keys(this.errorCodesStatusMapping).includes(exception.code)) { + return super.catch(exception, host); + } + const errorResponse = { + message: message, + statusCode: statusCode, + }; + response.status(statusCode).send(errorResponse); + } + return new HttpException({ statusCode, message }, statusCode); + } + + /** + * @param exception + * @returns short message for the exception + */ + exceptionShortMessage(message: string): string { + const shortMessage = message.substring(message.indexOf("→")); + return shortMessage + .substring(shortMessage.indexOf("\n")) + .replace(/\n/g, "") + .trim(); + } +} diff --git a/apps/order-service/src/grants.json b/apps/order-service/src/grants.json new file mode 100644 index 0000000..dc1240c --- /dev/null +++ b/apps/order-service/src/grants.json @@ -0,0 +1,140 @@ +[ + { + "role": "user", + "resource": "User", + "action": "read:own", + "attributes": "*" + }, + { + "role": "user", + "resource": "User", + "action": "create:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "User", + "action": "update:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "User", + "action": "delete:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "User", + "action": "read:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Order", + "action": "read:own", + "attributes": "*" + }, + { + "role": "user", + "resource": "Order", + "action": "create:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Order", + "action": "update:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Order", + "action": "delete:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Order", + "action": "read:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Customer", + "action": "create:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Customer", + "action": "update:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Customer", + "action": "delete:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Address", + "action": "read:own", + "attributes": "*" + }, + { + "role": "user", + "resource": "Address", + "action": "create:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Address", + "action": "update:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Address", + "action": "delete:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Address", + "action": "read:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Product", + "action": "read:own", + "attributes": "*" + }, + { + "role": "user", + "resource": "Product", + "action": "create:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Product", + "action": "update:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Product", + "action": "delete:any", + "attributes": "*" + }, + { + "role": "user", + "resource": "Product", + "action": "read:any", + "attributes": "*" + } +] diff --git a/apps/order-service/src/health/base/health.controller.base.ts b/apps/order-service/src/health/base/health.controller.base.ts new file mode 100644 index 0000000..afd9e0d --- /dev/null +++ b/apps/order-service/src/health/base/health.controller.base.ts @@ -0,0 +1,19 @@ +import { Get, HttpStatus, Res } from "@nestjs/common"; +import { Response } from "express"; +import { HealthService } from "../health.service"; + +export class HealthControllerBase { + constructor(protected readonly healthService: HealthService) {} + @Get("live") + healthLive(@Res() response: Response): Response<void> { + return response.status(HttpStatus.NO_CONTENT).send(); + } + @Get("ready") + async healthReady(@Res() response: Response): Promise<Response<void>> { + const dbConnection = await this.healthService.isDbReady(); + if (!dbConnection) { + return response.status(HttpStatus.NOT_FOUND).send(); + } + return response.status(HttpStatus.NO_CONTENT).send(); + } +} diff --git a/apps/order-service/src/health/base/health.service.base.ts b/apps/order-service/src/health/base/health.service.base.ts new file mode 100644 index 0000000..49a93a5 --- /dev/null +++ b/apps/order-service/src/health/base/health.service.base.ts @@ -0,0 +1,15 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../../prisma/prisma.service"; + +@Injectable() +export class HealthServiceBase { + constructor(protected readonly prisma: PrismaService) {} + async isDbReady(): Promise<boolean> { + try { + await this.prisma.$queryRaw`SELECT 1`; + return true; + } catch (error) { + return false; + } + } +} diff --git a/apps/order-service/src/health/health.controller.ts b/apps/order-service/src/health/health.controller.ts new file mode 100644 index 0000000..ff484e7 --- /dev/null +++ b/apps/order-service/src/health/health.controller.ts @@ -0,0 +1,10 @@ +import { Controller } from "@nestjs/common"; +import { HealthControllerBase } from "./base/health.controller.base"; +import { HealthService } from "./health.service"; + +@Controller("_health") +export class HealthController extends HealthControllerBase { + constructor(protected readonly healthService: HealthService) { + super(healthService); + } +} diff --git a/apps/order-service/src/health/health.module.ts b/apps/order-service/src/health/health.module.ts new file mode 100644 index 0000000..39eff7f --- /dev/null +++ b/apps/order-service/src/health/health.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { HealthController } from "./health.controller"; +import { HealthService } from "./health.service"; + +@Module({ + controllers: [HealthController], + providers: [HealthService], + exports: [HealthService], +}) +export class HealthModule {} diff --git a/apps/order-service/src/health/health.service.ts b/apps/order-service/src/health/health.service.ts new file mode 100644 index 0000000..44d9343 --- /dev/null +++ b/apps/order-service/src/health/health.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { HealthServiceBase } from "./base/health.service.base"; + +@Injectable() +export class HealthService extends HealthServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/apps/order-service/src/interceptors/aclFilterResponse.interceptor.ts b/apps/order-service/src/interceptors/aclFilterResponse.interceptor.ts new file mode 100644 index 0000000..5eeba18 --- /dev/null +++ b/apps/order-service/src/interceptors/aclFilterResponse.interceptor.ts @@ -0,0 +1,42 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from "@nestjs/common"; +import { Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { InjectRolesBuilder, RolesBuilder } from "nest-access-control"; +import { Reflector } from "@nestjs/core"; + +@Injectable() +export class AclFilterResponseInterceptor implements NestInterceptor { + constructor( + @InjectRolesBuilder() private readonly rolesBuilder: RolesBuilder, + private readonly reflector: Reflector + ) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable<any> { + const [permissionsRoles]: any = this.reflector.getAllAndMerge<string[]>( + "roles", + [context.getHandler(), context.getClass()] + ); + + const permission = this.rolesBuilder.permission({ + role: permissionsRoles.role, + action: permissionsRoles.action, + possession: permissionsRoles.possession, + resource: permissionsRoles.resource, + }); + + return next.handle().pipe( + map((data) => { + if (Array.isArray(data)) { + return data.map((results: any) => permission.filter(results)); + } else { + return permission.filter(data); + } + }) + ); + } +} diff --git a/apps/order-service/src/interceptors/aclValidateRequest.interceptor.ts b/apps/order-service/src/interceptors/aclValidateRequest.interceptor.ts new file mode 100644 index 0000000..6d30246 --- /dev/null +++ b/apps/order-service/src/interceptors/aclValidateRequest.interceptor.ts @@ -0,0 +1,53 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from "@nestjs/common"; +import { Observable } from "rxjs"; +import { InjectRolesBuilder, RolesBuilder } from "nest-access-control"; +import { Reflector } from "@nestjs/core"; +import * as abacUtil from "../auth/abac.util"; +import { ForbiddenException } from "../errors"; + +@Injectable() +export class AclValidateRequestInterceptor implements NestInterceptor { + constructor( + @InjectRolesBuilder() private readonly rolesBuilder: RolesBuilder, + private readonly reflector: Reflector + ) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable<any> { + const [permissionsRoles]: any = this.reflector.getAllAndMerge<string[]>( + "roles", + [context.getHandler(), context.getClass()] + ); + + const type = context.getType(); + + const inputDataToValidate = + type === "http" + ? context.switchToHttp().getRequest().body + : context.getArgByIndex(1).data; + + const permission = this.rolesBuilder.permission({ + role: permissionsRoles.role, + action: permissionsRoles.action, + possession: permissionsRoles.possession, + resource: permissionsRoles.resource, + }); + + const invalidAttributes = abacUtil.getInvalidAttributes( + permission, + inputDataToValidate + ); + + if (invalidAttributes.length) { + throw new ForbiddenException( + "Insufficient privileges to complete the operation" + ); + } + + return next.handle(); + } +} diff --git a/apps/order-service/src/kafka/KafkaMessage.ts b/apps/order-service/src/kafka/KafkaMessage.ts new file mode 100644 index 0000000..8e53a13 --- /dev/null +++ b/apps/order-service/src/kafka/KafkaMessage.ts @@ -0,0 +1,7 @@ +import { KafkaMessageHeaders } from "./KafkaMessageHeaders"; + +export interface KafkaMessage { + key: string | Record<string, any> | null; + value: string | Record<string, any>; + headers?: KafkaMessageHeaders; +} diff --git a/apps/order-service/src/kafka/KafkaMessageHeaders.ts b/apps/order-service/src/kafka/KafkaMessageHeaders.ts new file mode 100644 index 0000000..0fbe06e --- /dev/null +++ b/apps/order-service/src/kafka/KafkaMessageHeaders.ts @@ -0,0 +1,3 @@ +export interface KafkaMessageHeaders { + [key: string]: Buffer | string | undefined; +} diff --git a/apps/order-service/src/kafka/generateKafkaClientOptions.ts b/apps/order-service/src/kafka/generateKafkaClientOptions.ts new file mode 100644 index 0000000..c04c5bb --- /dev/null +++ b/apps/order-service/src/kafka/generateKafkaClientOptions.ts @@ -0,0 +1,41 @@ +import { ConfigService } from "@nestjs/config"; +import { KafkaOptions, Transport } from "@nestjs/microservices"; + +export const generateKafkaClientOptions = ( + configService: ConfigService +): KafkaOptions => { + const kafkaBrokersString = configService.get("KAFKA_BROKERS"); + const kafkaEnableSSL = configService.get("KAFKA_ENABLE_SSL") === "true"; + const kafkaClientId = configService.get("KAFKA_CLIENT_ID"); + const kafkaGroupId = configService.get("KAFKA_GROUP_ID"); + + if (!kafkaBrokersString) { + throw new Error("KAFKA_BROKERS environment variable must be defined"); + } + + if (!kafkaClientId) { + throw new Error("KAFKA_CLIENT_ID environment variable must be defined"); + } + + if (!kafkaGroupId) { + throw new Error("KAFKA_GROUP_ID environment variable must be defined"); + } + + return { + transport: Transport.KAFKA, + options: { + client: { + clientId: kafkaClientId, + brokers: [...kafkaBrokersString.split(",")], + ssl: kafkaEnableSSL, + }, + producer: { + metadataMaxAge: 3000, + }, + consumer: { + rebalanceTimeout: 3000, + groupId: kafkaGroupId, + }, + }, + }; +}; diff --git a/apps/order-service/src/kafka/kafka.controller.ts b/apps/order-service/src/kafka/kafka.controller.ts new file mode 100644 index 0000000..38cccf8 --- /dev/null +++ b/apps/order-service/src/kafka/kafka.controller.ts @@ -0,0 +1,10 @@ +import { + Ctx, + EventPattern, + KafkaContext, + Payload, +} from "@nestjs/microservices"; +import { Controller } from "@nestjs/common"; + +@Controller("kafka-controller") +export class KafkaController {} diff --git a/apps/order-service/src/kafka/kafka.module.ts b/apps/order-service/src/kafka/kafka.module.ts new file mode 100644 index 0000000..3f2cc69 --- /dev/null +++ b/apps/order-service/src/kafka/kafka.module.ts @@ -0,0 +1,26 @@ +import { Global, Module } from "@nestjs/common"; +import { ClientProxyFactory } from "@nestjs/microservices"; +import { generateKafkaClientOptions } from "./generateKafkaClientOptions"; +import { KafkaProducerService } from "./kafka.producer.service"; +import { KafkaController } from "./kafka.controller"; +import { ConfigService } from "@nestjs/config"; + +@Global() +@Module({ + imports: [], + providers: [ + { + provide: "KAFKA_CLIENT", + useFactory: (configService: ConfigService) => { + return ClientProxyFactory.create( + generateKafkaClientOptions(configService) + ); + }, + inject: [ConfigService], + }, + KafkaProducerService, + ], + controllers: [KafkaController], + exports: [KafkaProducerService], +}) +export class KafkaModule {} diff --git a/apps/order-service/src/kafka/kafka.producer.service.ts b/apps/order-service/src/kafka/kafka.producer.service.ts new file mode 100644 index 0000000..c921b1a --- /dev/null +++ b/apps/order-service/src/kafka/kafka.producer.service.ts @@ -0,0 +1,29 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { ClientKafka } from "@nestjs/microservices"; +import { KafkaMessage } from "./KafkaMessage"; +import { MyMessageBrokerTopics } from "./topics"; + +@Injectable() +export class KafkaProducerService { + constructor(@Inject("KAFKA_CLIENT") private kafkaClient: ClientKafka) {} + + async emitMessage( + topic: MyMessageBrokerTopics, + message: KafkaMessage + ): Promise<void> { + return await new Promise((resolve, reject) => { + this.kafkaClient.emit(topic, message).subscribe({ + error: (err: Error) => { + reject(err); + }, + next: () => { + resolve(); + }, + }); + }); + } + + async onModuleInit() { + await this.kafkaClient.connect(); + } +} diff --git a/apps/order-service/src/kafka/topics.ts b/apps/order-service/src/kafka/topics.ts new file mode 100644 index 0000000..f7db5ac --- /dev/null +++ b/apps/order-service/src/kafka/topics.ts @@ -0,0 +1 @@ +export enum MyMessageBrokerTopics {} diff --git a/apps/order-service/src/main.ts b/apps/order-service/src/main.ts new file mode 100644 index 0000000..474eead --- /dev/null +++ b/apps/order-service/src/main.ts @@ -0,0 +1,53 @@ +import { ValidationPipe } from "@nestjs/common"; +import { HttpAdapterHost, NestFactory } from "@nestjs/core"; +import { OpenAPIObject, SwaggerModule } from "@nestjs/swagger"; +import { HttpExceptionFilter } from "./filters/HttpExceptions.filter"; +import { AppModule } from "./app.module"; +import { connectMicroservices } from "./connectMicroservices"; +import { + swaggerPath, + swaggerDocumentOptions, + swaggerSetupOptions, +} from "./swagger"; + +const { PORT = 3000 } = process.env; + +async function main() { + const app = await NestFactory.create(AppModule, { cors: true }); + + app.setGlobalPrefix("api"); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + forbidUnknownValues: false, + }) + ); + + const document = SwaggerModule.createDocument(app, swaggerDocumentOptions); + + /** check if there is Public decorator for each path (action) and its method (findMany / findOne) on each controller */ + Object.values((document as OpenAPIObject).paths).forEach((path: any) => { + Object.values(path).forEach((method: any) => { + if ( + Array.isArray(method.security) && + method.security.includes("isPublic") + ) { + method.security = []; + } + }); + }); + + await connectMicroservices(app); + await app.startAllMicroservices(); + + SwaggerModule.setup(swaggerPath, app, document, swaggerSetupOptions); + + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new HttpExceptionFilter(httpAdapter)); + + void app.listen(PORT); + + return app; +} + +module.exports = main(); diff --git a/apps/order-service/src/order/base/CreateOrderArgs.ts b/apps/order-service/src/order/base/CreateOrderArgs.ts new file mode 100644 index 0000000..f320b6e --- /dev/null +++ b/apps/order-service/src/order/base/CreateOrderArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { OrderCreateInput } from "./OrderCreateInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CreateOrderArgs { + @ApiProperty({ + required: true, + type: () => OrderCreateInput, + }) + @ValidateNested() + @Type(() => OrderCreateInput) + @Field(() => OrderCreateInput, { nullable: false }) + data!: OrderCreateInput; +} + +export { CreateOrderArgs as CreateOrderArgs }; diff --git a/apps/order-service/src/order/base/DeleteOrderArgs.ts b/apps/order-service/src/order/base/DeleteOrderArgs.ts new file mode 100644 index 0000000..b73893f --- /dev/null +++ b/apps/order-service/src/order/base/DeleteOrderArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { OrderWhereUniqueInput } from "./OrderWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class DeleteOrderArgs { + @ApiProperty({ + required: true, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @Field(() => OrderWhereUniqueInput, { nullable: false }) + where!: OrderWhereUniqueInput; +} + +export { DeleteOrderArgs as DeleteOrderArgs }; diff --git a/apps/order-service/src/order/base/Order.ts b/apps/order-service/src/order/base/Order.ts new file mode 100644 index 0000000..beeeca9 --- /dev/null +++ b/apps/order-service/src/order/base/Order.ts @@ -0,0 +1,112 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { + IsDate, + ValidateNested, + IsOptional, + IsNumber, + Min, + Max, + IsString, + IsInt, +} from "class-validator"; +import { Type } from "class-transformer"; +import { Customer } from "../../customer/base/Customer"; +import { Product } from "../../product/base/Product"; + +@ObjectType() +class Order { + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + createdAt!: Date; + + @ApiProperty({ + required: false, + type: () => Customer, + }) + @ValidateNested() + @Type(() => Customer) + @IsOptional() + customer?: Customer | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsNumber() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + discount!: number | null; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; + + @ApiProperty({ + required: false, + type: () => Product, + }) + @ValidateNested() + @Type(() => Product) + @IsOptional() + product?: Product | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + quantity!: number | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + totalPrice!: number | null; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + updatedAt!: Date; +} + +export { Order as Order }; diff --git a/apps/order-service/src/order/base/OrderCountArgs.ts b/apps/order-service/src/order/base/OrderCountArgs.ts new file mode 100644 index 0000000..ef1ddad --- /dev/null +++ b/apps/order-service/src/order/base/OrderCountArgs.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { OrderWhereInput } from "./OrderWhereInput"; +import { Type } from "class-transformer"; + +@ArgsType() +class OrderCountArgs { + @ApiProperty({ + required: false, + type: () => OrderWhereInput, + }) + @Field(() => OrderWhereInput, { nullable: true }) + @Type(() => OrderWhereInput) + where?: OrderWhereInput; +} + +export { OrderCountArgs as OrderCountArgs }; diff --git a/apps/order-service/src/order/base/OrderCreateInput.ts b/apps/order-service/src/order/base/OrderCreateInput.ts new file mode 100644 index 0000000..d574482 --- /dev/null +++ b/apps/order-service/src/order/base/OrderCreateInput.ts @@ -0,0 +1,92 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereUniqueInput } from "../../customer/base/CustomerWhereUniqueInput"; +import { + ValidateNested, + IsOptional, + IsNumber, + Min, + Max, + IsInt, +} from "class-validator"; +import { Type } from "class-transformer"; +import { ProductWhereUniqueInput } from "../../product/base/ProductWhereUniqueInput"; + +@InputType() +class OrderCreateInput { + @ApiProperty({ + required: false, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @IsOptional() + @Field(() => CustomerWhereUniqueInput, { + nullable: true, + }) + customer?: CustomerWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsNumber() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + discount?: number | null; + + @ApiProperty({ + required: false, + type: () => ProductWhereUniqueInput, + }) + @ValidateNested() + @Type(() => ProductWhereUniqueInput) + @IsOptional() + @Field(() => ProductWhereUniqueInput, { + nullable: true, + }) + product?: ProductWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + quantity?: number | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + totalPrice?: number | null; +} + +export { OrderCreateInput as OrderCreateInput }; diff --git a/apps/order-service/src/order/base/OrderFindManyArgs.ts b/apps/order-service/src/order/base/OrderFindManyArgs.ts new file mode 100644 index 0000000..0e13627 --- /dev/null +++ b/apps/order-service/src/order/base/OrderFindManyArgs.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { OrderWhereInput } from "./OrderWhereInput"; +import { IsOptional, ValidateNested, IsInt } from "class-validator"; +import { Type } from "class-transformer"; +import { OrderOrderByInput } from "./OrderOrderByInput"; + +@ArgsType() +class OrderFindManyArgs { + @ApiProperty({ + required: false, + type: () => OrderWhereInput, + }) + @IsOptional() + @ValidateNested() + @Field(() => OrderWhereInput, { nullable: true }) + @Type(() => OrderWhereInput) + where?: OrderWhereInput; + + @ApiProperty({ + required: false, + type: [OrderOrderByInput], + }) + @IsOptional() + @ValidateNested({ each: true }) + @Field(() => [OrderOrderByInput], { nullable: true }) + @Type(() => OrderOrderByInput) + orderBy?: Array<OrderOrderByInput>; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + skip?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + take?: number; +} + +export { OrderFindManyArgs as OrderFindManyArgs }; diff --git a/apps/order-service/src/order/base/OrderFindUniqueArgs.ts b/apps/order-service/src/order/base/OrderFindUniqueArgs.ts new file mode 100644 index 0000000..0b330ab --- /dev/null +++ b/apps/order-service/src/order/base/OrderFindUniqueArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { OrderWhereUniqueInput } from "./OrderWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class OrderFindUniqueArgs { + @ApiProperty({ + required: true, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @Field(() => OrderWhereUniqueInput, { nullable: false }) + where!: OrderWhereUniqueInput; +} + +export { OrderFindUniqueArgs as OrderFindUniqueArgs }; diff --git a/apps/order-service/src/order/base/OrderListRelationFilter.ts b/apps/order-service/src/order/base/OrderListRelationFilter.ts new file mode 100644 index 0000000..0ecb03a --- /dev/null +++ b/apps/order-service/src/order/base/OrderListRelationFilter.ts @@ -0,0 +1,56 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { OrderWhereInput } from "./OrderWhereInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class OrderListRelationFilter { + @ApiProperty({ + required: false, + type: () => OrderWhereInput, + }) + @ValidateNested() + @Type(() => OrderWhereInput) + @IsOptional() + @Field(() => OrderWhereInput, { + nullable: true, + }) + every?: OrderWhereInput; + + @ApiProperty({ + required: false, + type: () => OrderWhereInput, + }) + @ValidateNested() + @Type(() => OrderWhereInput) + @IsOptional() + @Field(() => OrderWhereInput, { + nullable: true, + }) + some?: OrderWhereInput; + + @ApiProperty({ + required: false, + type: () => OrderWhereInput, + }) + @ValidateNested() + @Type(() => OrderWhereInput) + @IsOptional() + @Field(() => OrderWhereInput, { + nullable: true, + }) + none?: OrderWhereInput; +} +export { OrderListRelationFilter as OrderListRelationFilter }; diff --git a/apps/order-service/src/order/base/OrderOrderByInput.ts b/apps/order-service/src/order/base/OrderOrderByInput.ts new file mode 100644 index 0000000..9c674b4 --- /dev/null +++ b/apps/order-service/src/order/base/OrderOrderByInput.ts @@ -0,0 +1,111 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsEnum } from "class-validator"; +import { SortOrder } from "../../util/SortOrder"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +class OrderOrderByInput { + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + createdAt?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + customerId?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + discount?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + id?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + productId?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + quantity?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + totalPrice?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + updatedAt?: SortOrder; +} + +export { OrderOrderByInput as OrderOrderByInput }; diff --git a/apps/order-service/src/order/base/OrderUpdateInput.ts b/apps/order-service/src/order/base/OrderUpdateInput.ts new file mode 100644 index 0000000..b40ef38 --- /dev/null +++ b/apps/order-service/src/order/base/OrderUpdateInput.ts @@ -0,0 +1,92 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereUniqueInput } from "../../customer/base/CustomerWhereUniqueInput"; +import { + ValidateNested, + IsOptional, + IsNumber, + Min, + Max, + IsInt, +} from "class-validator"; +import { Type } from "class-transformer"; +import { ProductWhereUniqueInput } from "../../product/base/ProductWhereUniqueInput"; + +@InputType() +class OrderUpdateInput { + @ApiProperty({ + required: false, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @IsOptional() + @Field(() => CustomerWhereUniqueInput, { + nullable: true, + }) + customer?: CustomerWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsNumber() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + discount?: number | null; + + @ApiProperty({ + required: false, + type: () => ProductWhereUniqueInput, + }) + @ValidateNested() + @Type(() => ProductWhereUniqueInput) + @IsOptional() + @Field(() => ProductWhereUniqueInput, { + nullable: true, + }) + product?: ProductWhereUniqueInput | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + quantity?: number | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsInt() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + totalPrice?: number | null; +} + +export { OrderUpdateInput as OrderUpdateInput }; diff --git a/apps/order-service/src/order/base/OrderWhereInput.ts b/apps/order-service/src/order/base/OrderWhereInput.ts new file mode 100644 index 0000000..376134b --- /dev/null +++ b/apps/order-service/src/order/base/OrderWhereInput.ts @@ -0,0 +1,93 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { CustomerWhereUniqueInput } from "../../customer/base/CustomerWhereUniqueInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +import { FloatNullableFilter } from "../../util/FloatNullableFilter"; +import { StringFilter } from "../../util/StringFilter"; +import { ProductWhereUniqueInput } from "../../product/base/ProductWhereUniqueInput"; +import { IntNullableFilter } from "../../util/IntNullableFilter"; + +@InputType() +class OrderWhereInput { + @ApiProperty({ + required: false, + type: () => CustomerWhereUniqueInput, + }) + @ValidateNested() + @Type(() => CustomerWhereUniqueInput) + @IsOptional() + @Field(() => CustomerWhereUniqueInput, { + nullable: true, + }) + customer?: CustomerWhereUniqueInput; + + @ApiProperty({ + required: false, + type: FloatNullableFilter, + }) + @Type(() => FloatNullableFilter) + @IsOptional() + @Field(() => FloatNullableFilter, { + nullable: true, + }) + discount?: FloatNullableFilter; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + id?: StringFilter; + + @ApiProperty({ + required: false, + type: () => ProductWhereUniqueInput, + }) + @ValidateNested() + @Type(() => ProductWhereUniqueInput) + @IsOptional() + @Field(() => ProductWhereUniqueInput, { + nullable: true, + }) + product?: ProductWhereUniqueInput; + + @ApiProperty({ + required: false, + type: IntNullableFilter, + }) + @Type(() => IntNullableFilter) + @IsOptional() + @Field(() => IntNullableFilter, { + nullable: true, + }) + quantity?: IntNullableFilter; + + @ApiProperty({ + required: false, + type: IntNullableFilter, + }) + @Type(() => IntNullableFilter) + @IsOptional() + @Field(() => IntNullableFilter, { + nullable: true, + }) + totalPrice?: IntNullableFilter; +} + +export { OrderWhereInput as OrderWhereInput }; diff --git a/apps/order-service/src/order/base/OrderWhereUniqueInput.ts b/apps/order-service/src/order/base/OrderWhereUniqueInput.ts new file mode 100644 index 0000000..04690e2 --- /dev/null +++ b/apps/order-service/src/order/base/OrderWhereUniqueInput.ts @@ -0,0 +1,27 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +@InputType() +class OrderWhereUniqueInput { + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; +} + +export { OrderWhereUniqueInput as OrderWhereUniqueInput }; diff --git a/apps/order-service/src/order/base/UpdateOrderArgs.ts b/apps/order-service/src/order/base/UpdateOrderArgs.ts new file mode 100644 index 0000000..8eed9f2 --- /dev/null +++ b/apps/order-service/src/order/base/UpdateOrderArgs.ts @@ -0,0 +1,40 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { OrderWhereUniqueInput } from "./OrderWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { OrderUpdateInput } from "./OrderUpdateInput"; + +@ArgsType() +class UpdateOrderArgs { + @ApiProperty({ + required: true, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @Field(() => OrderWhereUniqueInput, { nullable: false }) + where!: OrderWhereUniqueInput; + + @ApiProperty({ + required: true, + type: () => OrderUpdateInput, + }) + @ValidateNested() + @Type(() => OrderUpdateInput) + @Field(() => OrderUpdateInput, { nullable: false }) + data!: OrderUpdateInput; +} + +export { UpdateOrderArgs as UpdateOrderArgs }; diff --git a/apps/order-service/src/order/base/order.controller.base.spec.ts b/apps/order-service/src/order/base/order.controller.base.spec.ts new file mode 100644 index 0000000..f954e6b --- /dev/null +++ b/apps/order-service/src/order/base/order.controller.base.spec.ts @@ -0,0 +1,202 @@ +import { Test } from "@nestjs/testing"; +import { + INestApplication, + HttpStatus, + ExecutionContext, + CallHandler, +} from "@nestjs/common"; +import request from "supertest"; +import { ACGuard } from "nest-access-control"; +import { DefaultAuthGuard } from "../../auth/defaultAuth.guard"; +import { ACLModule } from "../../auth/acl.module"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { map } from "rxjs"; +import { OrderController } from "../order.controller"; +import { OrderService } from "../order.service"; + +const nonExistingId = "nonExistingId"; +const existingId = "existingId"; +const CREATE_INPUT = { + createdAt: new Date(), + discount: 42.42, + id: "exampleId", + quantity: 42, + totalPrice: 42, + updatedAt: new Date(), +}; +const CREATE_RESULT = { + createdAt: new Date(), + discount: 42.42, + id: "exampleId", + quantity: 42, + totalPrice: 42, + updatedAt: new Date(), +}; +const FIND_MANY_RESULT = [ + { + createdAt: new Date(), + discount: 42.42, + id: "exampleId", + quantity: 42, + totalPrice: 42, + updatedAt: new Date(), + }, +]; +const FIND_ONE_RESULT = { + createdAt: new Date(), + discount: 42.42, + id: "exampleId", + quantity: 42, + totalPrice: 42, + updatedAt: new Date(), +}; + +const service = { + createOrder() { + return CREATE_RESULT; + }, + orders: () => FIND_MANY_RESULT, + order: ({ where }: { where: { id: string } }) => { + switch (where.id) { + case existingId: + return FIND_ONE_RESULT; + case nonExistingId: + return null; + } + }, +}; + +const basicAuthGuard = { + canActivate: (context: ExecutionContext) => { + const argumentHost = context.switchToHttp(); + const request = argumentHost.getRequest(); + request.user = { + roles: ["user"], + }; + return true; + }, +}; + +const acGuard = { + canActivate: () => { + return true; + }, +}; + +const aclFilterResponseInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle().pipe( + map((data) => { + return data; + }) + ); + }, +}; +const aclValidateRequestInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle(); + }, +}; + +describe("Order", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: OrderService, + useValue: service, + }, + ], + controllers: [OrderController], + imports: [ACLModule], + }) + .overrideGuard(DefaultAuthGuard) + .useValue(basicAuthGuard) + .overrideGuard(ACGuard) + .useValue(acGuard) + .overrideInterceptor(AclFilterResponseInterceptor) + .useValue(aclFilterResponseInterceptor) + .overrideInterceptor(AclValidateRequestInterceptor) + .useValue(aclValidateRequestInterceptor) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + test("POST /orders", async () => { + await request(app.getHttpServer()) + .post("/orders") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }); + }); + + test("GET /orders", async () => { + await request(app.getHttpServer()) + .get("/orders") + .expect(HttpStatus.OK) + .expect([ + { + ...FIND_MANY_RESULT[0], + createdAt: FIND_MANY_RESULT[0].createdAt.toISOString(), + updatedAt: FIND_MANY_RESULT[0].updatedAt.toISOString(), + }, + ]); + }); + + test("GET /orders/:id non existing", async () => { + await request(app.getHttpServer()) + .get(`${"/orders"}/${nonExistingId}`) + .expect(HttpStatus.NOT_FOUND) + .expect({ + statusCode: HttpStatus.NOT_FOUND, + message: `No resource was found for {"${"id"}":"${nonExistingId}"}`, + error: "Not Found", + }); + }); + + test("GET /orders/:id existing", async () => { + await request(app.getHttpServer()) + .get(`${"/orders"}/${existingId}`) + .expect(HttpStatus.OK) + .expect({ + ...FIND_ONE_RESULT, + createdAt: FIND_ONE_RESULT.createdAt.toISOString(), + updatedAt: FIND_ONE_RESULT.updatedAt.toISOString(), + }); + }); + + test("POST /orders existing resource", async () => { + const agent = request(app.getHttpServer()); + await agent + .post("/orders") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }) + .then(function () { + agent + .post("/orders") + .send(CREATE_INPUT) + .expect(HttpStatus.CONFLICT) + .expect({ + statusCode: HttpStatus.CONFLICT, + }); + }); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/apps/order-service/src/order/base/order.controller.base.ts b/apps/order-service/src/order/base/order.controller.base.ts new file mode 100644 index 0000000..a82cf7f --- /dev/null +++ b/apps/order-service/src/order/base/order.controller.base.ts @@ -0,0 +1,295 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { isRecordNotFoundError } from "../../prisma.util"; +import * as errors from "../../errors"; +import { Request } from "express"; +import { plainToClass } from "class-transformer"; +import { ApiNestedQuery } from "../../decorators/api-nested-query.decorator"; +import * as nestAccessControl from "nest-access-control"; +import * as defaultAuthGuard from "../../auth/defaultAuth.guard"; +import { OrderService } from "../order.service"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { OrderCreateInput } from "./OrderCreateInput"; +import { Order } from "./Order"; +import { OrderFindManyArgs } from "./OrderFindManyArgs"; +import { OrderWhereUniqueInput } from "./OrderWhereUniqueInput"; +import { OrderUpdateInput } from "./OrderUpdateInput"; + +@swagger.ApiBearerAuth() +@common.UseGuards(defaultAuthGuard.DefaultAuthGuard, nestAccessControl.ACGuard) +export class OrderControllerBase { + constructor( + protected readonly service: OrderService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Post() + @swagger.ApiCreatedResponse({ type: Order }) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "create", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async createOrder(@common.Body() data: OrderCreateInput): Promise<Order> { + return await this.service.createOrder({ + data: { + ...data, + + customer: data.customer + ? { + connect: data.customer, + } + : undefined, + + product: data.product + ? { + connect: data.product, + } + : undefined, + }, + select: { + createdAt: true, + + customer: { + select: { + id: true, + }, + }, + + discount: true, + id: true, + + product: { + select: { + id: true, + }, + }, + + quantity: true, + totalPrice: true, + updatedAt: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get() + @swagger.ApiOkResponse({ type: [Order] }) + @ApiNestedQuery(OrderFindManyArgs) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "read", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async orders(@common.Req() request: Request): Promise<Order[]> { + const args = plainToClass(OrderFindManyArgs, request.query); + return this.service.orders({ + ...args, + select: { + createdAt: true, + + customer: { + select: { + id: true, + }, + }, + + discount: true, + id: true, + + product: { + select: { + id: true, + }, + }, + + quantity: true, + totalPrice: true, + updatedAt: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get("/:id") + @swagger.ApiOkResponse({ type: Order }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "read", + possession: "own", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async order( + @common.Param() params: OrderWhereUniqueInput + ): Promise<Order | null> { + const result = await this.service.order({ + where: params, + select: { + createdAt: true, + + customer: { + select: { + id: true, + }, + }, + + discount: true, + id: true, + + product: { + select: { + id: true, + }, + }, + + quantity: true, + totalPrice: true, + updatedAt: true, + }, + }); + if (result === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Patch("/:id") + @swagger.ApiOkResponse({ type: Order }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "update", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async updateOrder( + @common.Param() params: OrderWhereUniqueInput, + @common.Body() data: OrderUpdateInput + ): Promise<Order | null> { + try { + return await this.service.updateOrder({ + where: params, + data: { + ...data, + + customer: data.customer + ? { + connect: data.customer, + } + : undefined, + + product: data.product + ? { + connect: data.product, + } + : undefined, + }, + select: { + createdAt: true, + + customer: { + select: { + id: true, + }, + }, + + discount: true, + id: true, + + product: { + select: { + id: true, + }, + }, + + quantity: true, + totalPrice: true, + updatedAt: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Delete("/:id") + @swagger.ApiOkResponse({ type: Order }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "delete", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async deleteOrder( + @common.Param() params: OrderWhereUniqueInput + ): Promise<Order | null> { + try { + return await this.service.deleteOrder({ + where: params, + select: { + createdAt: true, + + customer: { + select: { + id: true, + }, + }, + + discount: true, + id: true, + + product: { + select: { + id: true, + }, + }, + + quantity: true, + totalPrice: true, + updatedAt: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } +} diff --git a/apps/order-service/src/order/base/order.module.base.ts b/apps/order-service/src/order/base/order.module.base.ts new file mode 100644 index 0000000..83bfa3a --- /dev/null +++ b/apps/order-service/src/order/base/order.module.base.ts @@ -0,0 +1,18 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { Module } from "@nestjs/common"; +import { ACLModule } from "../../auth/acl.module"; +@Module({ + imports: [ACLModule], + exports: [ACLModule], +}) +export class OrderModuleBase {} diff --git a/apps/order-service/src/order/base/order.resolver.base.ts b/apps/order-service/src/order/base/order.resolver.base.ts new file mode 100644 index 0000000..1436a5e --- /dev/null +++ b/apps/order-service/src/order/base/order.resolver.base.ts @@ -0,0 +1,208 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as graphql from "@nestjs/graphql"; +import { GraphQLError } from "graphql"; +import { isRecordNotFoundError } from "../../prisma.util"; +import { MetaQueryPayload } from "../../util/MetaQueryPayload"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { Order } from "./Order"; +import { OrderCountArgs } from "./OrderCountArgs"; +import { OrderFindManyArgs } from "./OrderFindManyArgs"; +import { OrderFindUniqueArgs } from "./OrderFindUniqueArgs"; +import { CreateOrderArgs } from "./CreateOrderArgs"; +import { UpdateOrderArgs } from "./UpdateOrderArgs"; +import { DeleteOrderArgs } from "./DeleteOrderArgs"; +import { Customer } from "../../customer/base/Customer"; +import { Product } from "../../product/base/Product"; +import { OrderService } from "../order.service"; +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Order) +export class OrderResolverBase { + constructor( + protected readonly service: OrderService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + + @graphql.Query(() => MetaQueryPayload) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "read", + possession: "any", + }) + async _ordersMeta( + @graphql.Args() args: OrderCountArgs + ): Promise<MetaQueryPayload> { + const result = await this.service.count(args); + return { + count: result, + }; + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => [Order]) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "read", + possession: "any", + }) + async orders(@graphql.Args() args: OrderFindManyArgs): Promise<Order[]> { + return this.service.orders(args); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => Order, { nullable: true }) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "read", + possession: "own", + }) + async order( + @graphql.Args() args: OrderFindUniqueArgs + ): Promise<Order | null> { + const result = await this.service.order(args); + if (result === null) { + return null; + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Order) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "create", + possession: "any", + }) + async createOrder(@graphql.Args() args: CreateOrderArgs): Promise<Order> { + return await this.service.createOrder({ + ...args, + data: { + ...args.data, + + customer: args.data.customer + ? { + connect: args.data.customer, + } + : undefined, + + product: args.data.product + ? { + connect: args.data.product, + } + : undefined, + }, + }); + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Order) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "update", + possession: "any", + }) + async updateOrder( + @graphql.Args() args: UpdateOrderArgs + ): Promise<Order | null> { + try { + return await this.service.updateOrder({ + ...args, + data: { + ...args.data, + + customer: args.data.customer + ? { + connect: args.data.customer, + } + : undefined, + + product: args.data.product + ? { + connect: args.data.product, + } + : undefined, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.Mutation(() => Order) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "delete", + possession: "any", + }) + async deleteOrder( + @graphql.Args() args: DeleteOrderArgs + ): Promise<Order | null> { + try { + return await this.service.deleteOrder(args); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.ResolveField(() => Customer, { + nullable: true, + name: "customer", + }) + @nestAccessControl.UseRoles({ + resource: "Customer", + action: "read", + possession: "any", + }) + async getCustomer(@graphql.Parent() parent: Order): Promise<Customer | null> { + const result = await this.service.getCustomer(parent.id); + + if (!result) { + return null; + } + return result; + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.ResolveField(() => Product, { + nullable: true, + name: "product", + }) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "read", + possession: "any", + }) + async getProduct(@graphql.Parent() parent: Order): Promise<Product | null> { + const result = await this.service.getProduct(parent.id); + + if (!result) { + return null; + } + return result; + } +} diff --git a/apps/order-service/src/order/base/order.service.base.ts b/apps/order-service/src/order/base/order.service.base.ts new file mode 100644 index 0000000..16bb3db --- /dev/null +++ b/apps/order-service/src/order/base/order.service.base.ts @@ -0,0 +1,59 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { PrismaService } from "../../prisma/prisma.service"; + +import { + Prisma, + Order as PrismaOrder, + Customer as PrismaCustomer, + Product as PrismaProduct, +} from "@prisma/client"; + +export class OrderServiceBase { + constructor(protected readonly prisma: PrismaService) {} + + async count(args: Omit<Prisma.OrderCountArgs, "select">): Promise<number> { + return this.prisma.order.count(args); + } + + async orders(args: Prisma.OrderFindManyArgs): Promise<PrismaOrder[]> { + return this.prisma.order.findMany(args); + } + async order(args: Prisma.OrderFindUniqueArgs): Promise<PrismaOrder | null> { + return this.prisma.order.findUnique(args); + } + async createOrder(args: Prisma.OrderCreateArgs): Promise<PrismaOrder> { + return this.prisma.order.create(args); + } + async updateOrder(args: Prisma.OrderUpdateArgs): Promise<PrismaOrder> { + return this.prisma.order.update(args); + } + async deleteOrder(args: Prisma.OrderDeleteArgs): Promise<PrismaOrder> { + return this.prisma.order.delete(args); + } + + async getCustomer(parentId: string): Promise<PrismaCustomer | null> { + return this.prisma.order + .findUnique({ + where: { id: parentId }, + }) + .customer(); + } + + async getProduct(parentId: string): Promise<PrismaProduct | null> { + return this.prisma.order + .findUnique({ + where: { id: parentId }, + }) + .product(); + } +} diff --git a/apps/order-service/src/order/order.controller.ts b/apps/order-service/src/order/order.controller.ts new file mode 100644 index 0000000..07002fd --- /dev/null +++ b/apps/order-service/src/order/order.controller.ts @@ -0,0 +1,17 @@ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import * as nestAccessControl from "nest-access-control"; +import { OrderService } from "./order.service"; +import { OrderControllerBase } from "./base/order.controller.base"; + +@swagger.ApiTags("orders") +@common.Controller("orders") +export class OrderController extends OrderControllerBase { + constructor( + protected readonly service: OrderService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/order/order.module.ts b/apps/order-service/src/order/order.module.ts new file mode 100644 index 0000000..468cd07 --- /dev/null +++ b/apps/order-service/src/order/order.module.ts @@ -0,0 +1,14 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module"; +import { OrderModuleBase } from "./base/order.module.base"; +import { OrderService } from "./order.service"; +import { OrderController } from "./order.controller"; +import { OrderResolver } from "./order.resolver"; + +@Module({ + imports: [OrderModuleBase, forwardRef(() => AuthModule)], + controllers: [OrderController], + providers: [OrderService, OrderResolver], + exports: [OrderService], +}) +export class OrderModule {} diff --git a/apps/order-service/src/order/order.resolver.ts b/apps/order-service/src/order/order.resolver.ts new file mode 100644 index 0000000..f5dfa6f --- /dev/null +++ b/apps/order-service/src/order/order.resolver.ts @@ -0,0 +1,20 @@ +import * as graphql from "@nestjs/graphql"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { OrderResolverBase } from "./base/order.resolver.base"; +import { Order } from "./base/Order"; +import { OrderService } from "./order.service"; + +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Order) +export class OrderResolver extends OrderResolverBase { + constructor( + protected readonly service: OrderService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/order/order.service.ts b/apps/order-service/src/order/order.service.ts new file mode 100644 index 0000000..1a0b876 --- /dev/null +++ b/apps/order-service/src/order/order.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { OrderServiceBase } from "./base/order.service.base"; + +@Injectable() +export class OrderService extends OrderServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/apps/order-service/src/prisma.util.spec.ts b/apps/order-service/src/prisma.util.spec.ts new file mode 100644 index 0000000..0aa308e --- /dev/null +++ b/apps/order-service/src/prisma.util.spec.ts @@ -0,0 +1,23 @@ +import { + isRecordNotFoundError, + PRISMA_QUERY_INTERPRETATION_ERROR, +} from "./prisma.util"; + +describe("isRecordNotFoundError", () => { + test("returns true for record not found error", () => { + expect( + isRecordNotFoundError( + Object.assign( + new Error(`Error occurred during query execution: + InterpretationError("Error for binding '0': RecordNotFound("Record to update not found.")")`), + { + code: PRISMA_QUERY_INTERPRETATION_ERROR, + } + ) + ) + ).toBe(true); + }); + test("returns false for any other error", () => { + expect(isRecordNotFoundError(new Error())).toBe(false); + }); +}); diff --git a/apps/order-service/src/prisma.util.ts b/apps/order-service/src/prisma.util.ts new file mode 100644 index 0000000..029b98a --- /dev/null +++ b/apps/order-service/src/prisma.util.ts @@ -0,0 +1,29 @@ +export const PRISMA_QUERY_INTERPRETATION_ERROR = "P2016"; +export const PRISMA_RECORD_NOT_FOUND = "RecordNotFound"; + +export function isRecordNotFoundError(error: any): boolean { + return ( + error instanceof Error && + "code" in error && + error.code === PRISMA_QUERY_INTERPRETATION_ERROR && + error.message.includes(PRISMA_RECORD_NOT_FOUND) + ); +} + +export async function transformStringFieldUpdateInput< + T extends undefined | string | { set?: string } +>(input: T, transform: (input: string) => Promise<string>): Promise<T> { + if (typeof input === "object" && typeof input?.set === "string") { + return { set: await transform(input.set) } as T; + } + if (typeof input === "object") { + if (typeof input.set === "string") { + return { set: await transform(input.set) } as T; + } + return input; + } + if (typeof input === "string") { + return (await transform(input)) as T; + } + return input; +} diff --git a/apps/order-service/src/prisma/prisma.module.ts b/apps/order-service/src/prisma/prisma.module.ts new file mode 100644 index 0000000..1edbf95 --- /dev/null +++ b/apps/order-service/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from "@nestjs/common"; +import { PrismaService } from "./prisma.service"; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/apps/order-service/src/prisma/prisma.service.ts b/apps/order-service/src/prisma/prisma.service.ts new file mode 100644 index 0000000..79ea4fa --- /dev/null +++ b/apps/order-service/src/prisma/prisma.service.ts @@ -0,0 +1,9 @@ +import { Injectable, OnModuleInit, INestApplication } from "@nestjs/common"; +import { PrismaClient } from "@prisma/client"; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit { + async onModuleInit() { + await this.$connect(); + } +} diff --git a/apps/order-service/src/product/base/CreateProductArgs.ts b/apps/order-service/src/product/base/CreateProductArgs.ts new file mode 100644 index 0000000..72d659f --- /dev/null +++ b/apps/order-service/src/product/base/CreateProductArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProductCreateInput } from "./ProductCreateInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CreateProductArgs { + @ApiProperty({ + required: true, + type: () => ProductCreateInput, + }) + @ValidateNested() + @Type(() => ProductCreateInput) + @Field(() => ProductCreateInput, { nullable: false }) + data!: ProductCreateInput; +} + +export { CreateProductArgs as CreateProductArgs }; diff --git a/apps/order-service/src/product/base/DeleteProductArgs.ts b/apps/order-service/src/product/base/DeleteProductArgs.ts new file mode 100644 index 0000000..1bf3544 --- /dev/null +++ b/apps/order-service/src/product/base/DeleteProductArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProductWhereUniqueInput } from "./ProductWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class DeleteProductArgs { + @ApiProperty({ + required: true, + type: () => ProductWhereUniqueInput, + }) + @ValidateNested() + @Type(() => ProductWhereUniqueInput) + @Field(() => ProductWhereUniqueInput, { nullable: false }) + where!: ProductWhereUniqueInput; +} + +export { DeleteProductArgs as DeleteProductArgs }; diff --git a/apps/order-service/src/product/base/Product.ts b/apps/order-service/src/product/base/Product.ts new file mode 100644 index 0000000..bfdaf35 --- /dev/null +++ b/apps/order-service/src/product/base/Product.ts @@ -0,0 +1,102 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; + +import { + IsDate, + IsString, + MaxLength, + IsOptional, + IsNumber, + Min, + Max, + ValidateNested, +} from "class-validator"; + +import { Type } from "class-transformer"; +import { Order } from "../../order/base/Order"; + +@ObjectType() +class Product { + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + createdAt!: Date; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + description!: string | null; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsNumber() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + itemPrice!: number | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + name!: string | null; + + @ApiProperty({ + required: false, + type: () => Order, + }) + @ValidateNested() + @Type(() => Order) + @IsOptional() + orders?: Order | null; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + updatedAt!: Date; +} + +export { Product as Product }; diff --git a/apps/order-service/src/product/base/ProductCountArgs.ts b/apps/order-service/src/product/base/ProductCountArgs.ts new file mode 100644 index 0000000..f1bbaf5 --- /dev/null +++ b/apps/order-service/src/product/base/ProductCountArgs.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProductWhereInput } from "./ProductWhereInput"; +import { Type } from "class-transformer"; + +@ArgsType() +class ProductCountArgs { + @ApiProperty({ + required: false, + type: () => ProductWhereInput, + }) + @Field(() => ProductWhereInput, { nullable: true }) + @Type(() => ProductWhereInput) + where?: ProductWhereInput; +} + +export { ProductCountArgs as ProductCountArgs }; diff --git a/apps/order-service/src/product/base/ProductCreateInput.ts b/apps/order-service/src/product/base/ProductCreateInput.ts new file mode 100644 index 0000000..c62f6b0 --- /dev/null +++ b/apps/order-service/src/product/base/ProductCreateInput.ts @@ -0,0 +1,78 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { + IsString, + MaxLength, + IsOptional, + IsNumber, + Min, + Max, + ValidateNested, +} from "class-validator"; +import { OrderWhereUniqueInput } from "../../order/base/OrderWhereUniqueInput"; +import { Type } from "class-transformer"; + +@InputType() +class ProductCreateInput { + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + description?: string | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsNumber() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + itemPrice?: number | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + name?: string | null; + + @ApiProperty({ + required: false, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @IsOptional() + @Field(() => OrderWhereUniqueInput, { + nullable: true, + }) + orders?: OrderWhereUniqueInput | null; +} + +export { ProductCreateInput as ProductCreateInput }; diff --git a/apps/order-service/src/product/base/ProductFindManyArgs.ts b/apps/order-service/src/product/base/ProductFindManyArgs.ts new file mode 100644 index 0000000..dc1610c --- /dev/null +++ b/apps/order-service/src/product/base/ProductFindManyArgs.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProductWhereInput } from "./ProductWhereInput"; +import { IsOptional, ValidateNested, IsInt } from "class-validator"; +import { Type } from "class-transformer"; +import { ProductOrderByInput } from "./ProductOrderByInput"; + +@ArgsType() +class ProductFindManyArgs { + @ApiProperty({ + required: false, + type: () => ProductWhereInput, + }) + @IsOptional() + @ValidateNested() + @Field(() => ProductWhereInput, { nullable: true }) + @Type(() => ProductWhereInput) + where?: ProductWhereInput; + + @ApiProperty({ + required: false, + type: [ProductOrderByInput], + }) + @IsOptional() + @ValidateNested({ each: true }) + @Field(() => [ProductOrderByInput], { nullable: true }) + @Type(() => ProductOrderByInput) + orderBy?: Array<ProductOrderByInput>; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + skip?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + take?: number; +} + +export { ProductFindManyArgs as ProductFindManyArgs }; diff --git a/apps/order-service/src/product/base/ProductFindUniqueArgs.ts b/apps/order-service/src/product/base/ProductFindUniqueArgs.ts new file mode 100644 index 0000000..582af7f --- /dev/null +++ b/apps/order-service/src/product/base/ProductFindUniqueArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProductWhereUniqueInput } from "./ProductWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class ProductFindUniqueArgs { + @ApiProperty({ + required: true, + type: () => ProductWhereUniqueInput, + }) + @ValidateNested() + @Type(() => ProductWhereUniqueInput) + @Field(() => ProductWhereUniqueInput, { nullable: false }) + where!: ProductWhereUniqueInput; +} + +export { ProductFindUniqueArgs as ProductFindUniqueArgs }; diff --git a/apps/order-service/src/product/base/ProductListRelationFilter.ts b/apps/order-service/src/product/base/ProductListRelationFilter.ts new file mode 100644 index 0000000..cce7c64 --- /dev/null +++ b/apps/order-service/src/product/base/ProductListRelationFilter.ts @@ -0,0 +1,56 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProductWhereInput } from "./ProductWhereInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class ProductListRelationFilter { + @ApiProperty({ + required: false, + type: () => ProductWhereInput, + }) + @ValidateNested() + @Type(() => ProductWhereInput) + @IsOptional() + @Field(() => ProductWhereInput, { + nullable: true, + }) + every?: ProductWhereInput; + + @ApiProperty({ + required: false, + type: () => ProductWhereInput, + }) + @ValidateNested() + @Type(() => ProductWhereInput) + @IsOptional() + @Field(() => ProductWhereInput, { + nullable: true, + }) + some?: ProductWhereInput; + + @ApiProperty({ + required: false, + type: () => ProductWhereInput, + }) + @ValidateNested() + @Type(() => ProductWhereInput) + @IsOptional() + @Field(() => ProductWhereInput, { + nullable: true, + }) + none?: ProductWhereInput; +} +export { ProductListRelationFilter as ProductListRelationFilter }; diff --git a/apps/order-service/src/product/base/ProductOrderByInput.ts b/apps/order-service/src/product/base/ProductOrderByInput.ts new file mode 100644 index 0000000..849ebb1 --- /dev/null +++ b/apps/order-service/src/product/base/ProductOrderByInput.ts @@ -0,0 +1,100 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsEnum } from "class-validator"; +import { SortOrder } from "../../util/SortOrder"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +class ProductOrderByInput { + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + createdAt?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + description?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + id?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + itemPrice?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + name?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + ordersId?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + updatedAt?: SortOrder; +} + +export { ProductOrderByInput as ProductOrderByInput }; diff --git a/apps/order-service/src/product/base/ProductUpdateInput.ts b/apps/order-service/src/product/base/ProductUpdateInput.ts new file mode 100644 index 0000000..3db05fc --- /dev/null +++ b/apps/order-service/src/product/base/ProductUpdateInput.ts @@ -0,0 +1,78 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { + IsString, + MaxLength, + IsOptional, + IsNumber, + Min, + Max, + ValidateNested, +} from "class-validator"; +import { OrderWhereUniqueInput } from "../../order/base/OrderWhereUniqueInput"; +import { Type } from "class-transformer"; + +@InputType() +class ProductUpdateInput { + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + description?: string | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsNumber() + @Min(-999999999) + @Max(999999999) + @IsOptional() + @Field(() => Number, { + nullable: true, + }) + itemPrice?: number | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(1000) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + name?: string | null; + + @ApiProperty({ + required: false, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @IsOptional() + @Field(() => OrderWhereUniqueInput, { + nullable: true, + }) + orders?: OrderWhereUniqueInput | null; +} + +export { ProductUpdateInput as ProductUpdateInput }; diff --git a/apps/order-service/src/product/base/ProductWhereInput.ts b/apps/order-service/src/product/base/ProductWhereInput.ts new file mode 100644 index 0000000..5f66598 --- /dev/null +++ b/apps/order-service/src/product/base/ProductWhereInput.ts @@ -0,0 +1,80 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { Type } from "class-transformer"; +import { IsOptional, ValidateNested } from "class-validator"; +import { StringFilter } from "../../util/StringFilter"; +import { FloatNullableFilter } from "../../util/FloatNullableFilter"; +import { OrderWhereUniqueInput } from "../../order/base/OrderWhereUniqueInput"; + +@InputType() +class ProductWhereInput { + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + description?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + id?: StringFilter; + + @ApiProperty({ + required: false, + type: FloatNullableFilter, + }) + @Type(() => FloatNullableFilter) + @IsOptional() + @Field(() => FloatNullableFilter, { + nullable: true, + }) + itemPrice?: FloatNullableFilter; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + name?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: () => OrderWhereUniqueInput, + }) + @ValidateNested() + @Type(() => OrderWhereUniqueInput) + @IsOptional() + @Field(() => OrderWhereUniqueInput, { + nullable: true, + }) + orders?: OrderWhereUniqueInput; +} + +export { ProductWhereInput as ProductWhereInput }; diff --git a/apps/order-service/src/product/base/ProductWhereUniqueInput.ts b/apps/order-service/src/product/base/ProductWhereUniqueInput.ts new file mode 100644 index 0000000..b48170e --- /dev/null +++ b/apps/order-service/src/product/base/ProductWhereUniqueInput.ts @@ -0,0 +1,27 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +@InputType() +class ProductWhereUniqueInput { + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; +} + +export { ProductWhereUniqueInput as ProductWhereUniqueInput }; diff --git a/apps/order-service/src/product/base/UpdateProductArgs.ts b/apps/order-service/src/product/base/UpdateProductArgs.ts new file mode 100644 index 0000000..8ec7df8 --- /dev/null +++ b/apps/order-service/src/product/base/UpdateProductArgs.ts @@ -0,0 +1,40 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProductWhereUniqueInput } from "./ProductWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { ProductUpdateInput } from "./ProductUpdateInput"; + +@ArgsType() +class UpdateProductArgs { + @ApiProperty({ + required: true, + type: () => ProductWhereUniqueInput, + }) + @ValidateNested() + @Type(() => ProductWhereUniqueInput) + @Field(() => ProductWhereUniqueInput, { nullable: false }) + where!: ProductWhereUniqueInput; + + @ApiProperty({ + required: true, + type: () => ProductUpdateInput, + }) + @ValidateNested() + @Type(() => ProductUpdateInput) + @Field(() => ProductUpdateInput, { nullable: false }) + data!: ProductUpdateInput; +} + +export { UpdateProductArgs as UpdateProductArgs }; diff --git a/apps/order-service/src/product/base/product.controller.base.spec.ts b/apps/order-service/src/product/base/product.controller.base.spec.ts new file mode 100644 index 0000000..cfb4a79 --- /dev/null +++ b/apps/order-service/src/product/base/product.controller.base.spec.ts @@ -0,0 +1,202 @@ +import { Test } from "@nestjs/testing"; +import { + INestApplication, + HttpStatus, + ExecutionContext, + CallHandler, +} from "@nestjs/common"; +import request from "supertest"; +import { ACGuard } from "nest-access-control"; +import { DefaultAuthGuard } from "../../auth/defaultAuth.guard"; +import { ACLModule } from "../../auth/acl.module"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { map } from "rxjs"; +import { ProductController } from "../product.controller"; +import { ProductService } from "../product.service"; + +const nonExistingId = "nonExistingId"; +const existingId = "existingId"; +const CREATE_INPUT = { + createdAt: new Date(), + description: "exampleDescription", + id: "exampleId", + itemPrice: 42.42, + name: "exampleName", + updatedAt: new Date(), +}; +const CREATE_RESULT = { + createdAt: new Date(), + description: "exampleDescription", + id: "exampleId", + itemPrice: 42.42, + name: "exampleName", + updatedAt: new Date(), +}; +const FIND_MANY_RESULT = [ + { + createdAt: new Date(), + description: "exampleDescription", + id: "exampleId", + itemPrice: 42.42, + name: "exampleName", + updatedAt: new Date(), + }, +]; +const FIND_ONE_RESULT = { + createdAt: new Date(), + description: "exampleDescription", + id: "exampleId", + itemPrice: 42.42, + name: "exampleName", + updatedAt: new Date(), +}; + +const service = { + createProduct() { + return CREATE_RESULT; + }, + products: () => FIND_MANY_RESULT, + product: ({ where }: { where: { id: string } }) => { + switch (where.id) { + case existingId: + return FIND_ONE_RESULT; + case nonExistingId: + return null; + } + }, +}; + +const basicAuthGuard = { + canActivate: (context: ExecutionContext) => { + const argumentHost = context.switchToHttp(); + const request = argumentHost.getRequest(); + request.user = { + roles: ["user"], + }; + return true; + }, +}; + +const acGuard = { + canActivate: () => { + return true; + }, +}; + +const aclFilterResponseInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle().pipe( + map((data) => { + return data; + }) + ); + }, +}; +const aclValidateRequestInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle(); + }, +}; + +describe("Product", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: ProductService, + useValue: service, + }, + ], + controllers: [ProductController], + imports: [ACLModule], + }) + .overrideGuard(DefaultAuthGuard) + .useValue(basicAuthGuard) + .overrideGuard(ACGuard) + .useValue(acGuard) + .overrideInterceptor(AclFilterResponseInterceptor) + .useValue(aclFilterResponseInterceptor) + .overrideInterceptor(AclValidateRequestInterceptor) + .useValue(aclValidateRequestInterceptor) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + test("POST /products", async () => { + await request(app.getHttpServer()) + .post("/products") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }); + }); + + test("GET /products", async () => { + await request(app.getHttpServer()) + .get("/products") + .expect(HttpStatus.OK) + .expect([ + { + ...FIND_MANY_RESULT[0], + createdAt: FIND_MANY_RESULT[0].createdAt.toISOString(), + updatedAt: FIND_MANY_RESULT[0].updatedAt.toISOString(), + }, + ]); + }); + + test("GET /products/:id non existing", async () => { + await request(app.getHttpServer()) + .get(`${"/products"}/${nonExistingId}`) + .expect(HttpStatus.NOT_FOUND) + .expect({ + statusCode: HttpStatus.NOT_FOUND, + message: `No resource was found for {"${"id"}":"${nonExistingId}"}`, + error: "Not Found", + }); + }); + + test("GET /products/:id existing", async () => { + await request(app.getHttpServer()) + .get(`${"/products"}/${existingId}`) + .expect(HttpStatus.OK) + .expect({ + ...FIND_ONE_RESULT, + createdAt: FIND_ONE_RESULT.createdAt.toISOString(), + updatedAt: FIND_ONE_RESULT.updatedAt.toISOString(), + }); + }); + + test("POST /products existing resource", async () => { + const agent = request(app.getHttpServer()); + await agent + .post("/products") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }) + .then(function () { + agent + .post("/products") + .send(CREATE_INPUT) + .expect(HttpStatus.CONFLICT) + .expect({ + statusCode: HttpStatus.CONFLICT, + }); + }); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/apps/order-service/src/product/base/product.controller.base.ts b/apps/order-service/src/product/base/product.controller.base.ts new file mode 100644 index 0000000..9a9db9e --- /dev/null +++ b/apps/order-service/src/product/base/product.controller.base.ts @@ -0,0 +1,250 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { isRecordNotFoundError } from "../../prisma.util"; +import * as errors from "../../errors"; +import { Request } from "express"; +import { plainToClass } from "class-transformer"; +import { ApiNestedQuery } from "../../decorators/api-nested-query.decorator"; +import * as nestAccessControl from "nest-access-control"; +import * as defaultAuthGuard from "../../auth/defaultAuth.guard"; +import { ProductService } from "../product.service"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { ProductCreateInput } from "./ProductCreateInput"; +import { Product } from "./Product"; +import { ProductFindManyArgs } from "./ProductFindManyArgs"; +import { ProductWhereUniqueInput } from "./ProductWhereUniqueInput"; +import { ProductUpdateInput } from "./ProductUpdateInput"; + +@swagger.ApiBearerAuth() +@common.UseGuards(defaultAuthGuard.DefaultAuthGuard, nestAccessControl.ACGuard) +export class ProductControllerBase { + constructor( + protected readonly service: ProductService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Post() + @swagger.ApiCreatedResponse({ type: Product }) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "create", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async createProduct( + @common.Body() data: ProductCreateInput + ): Promise<Product> { + return await this.service.createProduct({ + data: { + ...data, + + orders: data.orders + ? { + connect: data.orders, + } + : undefined, + }, + select: { + createdAt: true, + description: true, + id: true, + itemPrice: true, + name: true, + + orders: { + select: { + id: true, + }, + }, + + updatedAt: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get() + @swagger.ApiOkResponse({ type: [Product] }) + @ApiNestedQuery(ProductFindManyArgs) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "read", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async products(@common.Req() request: Request): Promise<Product[]> { + const args = plainToClass(ProductFindManyArgs, request.query); + return this.service.products({ + ...args, + select: { + createdAt: true, + description: true, + id: true, + itemPrice: true, + name: true, + + orders: { + select: { + id: true, + }, + }, + + updatedAt: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get("/:id") + @swagger.ApiOkResponse({ type: Product }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "read", + possession: "own", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async product( + @common.Param() params: ProductWhereUniqueInput + ): Promise<Product | null> { + const result = await this.service.product({ + where: params, + select: { + createdAt: true, + description: true, + id: true, + itemPrice: true, + name: true, + + orders: { + select: { + id: true, + }, + }, + + updatedAt: true, + }, + }); + if (result === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Patch("/:id") + @swagger.ApiOkResponse({ type: Product }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "update", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async updateProduct( + @common.Param() params: ProductWhereUniqueInput, + @common.Body() data: ProductUpdateInput + ): Promise<Product | null> { + try { + return await this.service.updateProduct({ + where: params, + data: { + ...data, + + orders: data.orders + ? { + connect: data.orders, + } + : undefined, + }, + select: { + createdAt: true, + description: true, + id: true, + itemPrice: true, + name: true, + + orders: { + select: { + id: true, + }, + }, + + updatedAt: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Delete("/:id") + @swagger.ApiOkResponse({ type: Product }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "delete", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async deleteProduct( + @common.Param() params: ProductWhereUniqueInput + ): Promise<Product | null> { + try { + return await this.service.deleteProduct({ + where: params, + select: { + createdAt: true, + description: true, + id: true, + itemPrice: true, + name: true, + + orders: { + select: { + id: true, + }, + }, + + updatedAt: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } +} diff --git a/apps/order-service/src/product/base/product.module.base.ts b/apps/order-service/src/product/base/product.module.base.ts new file mode 100644 index 0000000..8d089c7 --- /dev/null +++ b/apps/order-service/src/product/base/product.module.base.ts @@ -0,0 +1,18 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { Module } from "@nestjs/common"; +import { ACLModule } from "../../auth/acl.module"; +@Module({ + imports: [ACLModule], + exports: [ACLModule], +}) +export class ProductModuleBase {} diff --git a/apps/order-service/src/product/base/product.resolver.base.ts b/apps/order-service/src/product/base/product.resolver.base.ts new file mode 100644 index 0000000..23d96de --- /dev/null +++ b/apps/order-service/src/product/base/product.resolver.base.ts @@ -0,0 +1,180 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as graphql from "@nestjs/graphql"; +import { GraphQLError } from "graphql"; +import { isRecordNotFoundError } from "../../prisma.util"; +import { MetaQueryPayload } from "../../util/MetaQueryPayload"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { Product } from "./Product"; +import { ProductCountArgs } from "./ProductCountArgs"; +import { ProductFindManyArgs } from "./ProductFindManyArgs"; +import { ProductFindUniqueArgs } from "./ProductFindUniqueArgs"; +import { CreateProductArgs } from "./CreateProductArgs"; +import { UpdateProductArgs } from "./UpdateProductArgs"; +import { DeleteProductArgs } from "./DeleteProductArgs"; +import { Order } from "../../order/base/Order"; +import { ProductService } from "../product.service"; +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Product) +export class ProductResolverBase { + constructor( + protected readonly service: ProductService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + + @graphql.Query(() => MetaQueryPayload) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "read", + possession: "any", + }) + async _productsMeta( + @graphql.Args() args: ProductCountArgs + ): Promise<MetaQueryPayload> { + const result = await this.service.count(args); + return { + count: result, + }; + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => [Product]) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "read", + possession: "any", + }) + async products( + @graphql.Args() args: ProductFindManyArgs + ): Promise<Product[]> { + return this.service.products(args); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => Product, { nullable: true }) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "read", + possession: "own", + }) + async product( + @graphql.Args() args: ProductFindUniqueArgs + ): Promise<Product | null> { + const result = await this.service.product(args); + if (result === null) { + return null; + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Product) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "create", + possession: "any", + }) + async createProduct( + @graphql.Args() args: CreateProductArgs + ): Promise<Product> { + return await this.service.createProduct({ + ...args, + data: { + ...args.data, + + orders: args.data.orders + ? { + connect: args.data.orders, + } + : undefined, + }, + }); + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => Product) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "update", + possession: "any", + }) + async updateProduct( + @graphql.Args() args: UpdateProductArgs + ): Promise<Product | null> { + try { + return await this.service.updateProduct({ + ...args, + data: { + ...args.data, + + orders: args.data.orders + ? { + connect: args.data.orders, + } + : undefined, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.Mutation(() => Product) + @nestAccessControl.UseRoles({ + resource: "Product", + action: "delete", + possession: "any", + }) + async deleteProduct( + @graphql.Args() args: DeleteProductArgs + ): Promise<Product | null> { + try { + return await this.service.deleteProduct(args); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.ResolveField(() => Order, { + nullable: true, + name: "orders", + }) + @nestAccessControl.UseRoles({ + resource: "Order", + action: "read", + possession: "any", + }) + async getOrders(@graphql.Parent() parent: Product): Promise<Order | null> { + const result = await this.service.getOrders(parent.id); + + if (!result) { + return null; + } + return result; + } +} diff --git a/apps/order-service/src/product/base/product.service.base.ts b/apps/order-service/src/product/base/product.service.base.ts new file mode 100644 index 0000000..9aa51ad --- /dev/null +++ b/apps/order-service/src/product/base/product.service.base.ts @@ -0,0 +1,51 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { PrismaService } from "../../prisma/prisma.service"; +import { + Prisma, + Product as PrismaProduct, + Order as PrismaOrder, +} from "@prisma/client"; + +export class ProductServiceBase { + constructor(protected readonly prisma: PrismaService) {} + + async count(args: Omit<Prisma.ProductCountArgs, "select">): Promise<number> { + return this.prisma.product.count(args); + } + + async products(args: Prisma.ProductFindManyArgs): Promise<PrismaProduct[]> { + return this.prisma.product.findMany(args); + } + async product( + args: Prisma.ProductFindUniqueArgs + ): Promise<PrismaProduct | null> { + return this.prisma.product.findUnique(args); + } + async createProduct(args: Prisma.ProductCreateArgs): Promise<PrismaProduct> { + return this.prisma.product.create(args); + } + async updateProduct(args: Prisma.ProductUpdateArgs): Promise<PrismaProduct> { + return this.prisma.product.update(args); + } + async deleteProduct(args: Prisma.ProductDeleteArgs): Promise<PrismaProduct> { + return this.prisma.product.delete(args); + } + + async getOrders(parentId: string): Promise<PrismaOrder | null> { + return this.prisma.product + .findUnique({ + where: { id: parentId }, + }) + .orders(); + } +} diff --git a/apps/order-service/src/product/product.controller.ts b/apps/order-service/src/product/product.controller.ts new file mode 100644 index 0000000..b7c034f --- /dev/null +++ b/apps/order-service/src/product/product.controller.ts @@ -0,0 +1,17 @@ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import * as nestAccessControl from "nest-access-control"; +import { ProductService } from "./product.service"; +import { ProductControllerBase } from "./base/product.controller.base"; + +@swagger.ApiTags("products") +@common.Controller("products") +export class ProductController extends ProductControllerBase { + constructor( + protected readonly service: ProductService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/product/product.module.ts b/apps/order-service/src/product/product.module.ts new file mode 100644 index 0000000..7520e33 --- /dev/null +++ b/apps/order-service/src/product/product.module.ts @@ -0,0 +1,14 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module"; +import { ProductModuleBase } from "./base/product.module.base"; +import { ProductService } from "./product.service"; +import { ProductController } from "./product.controller"; +import { ProductResolver } from "./product.resolver"; + +@Module({ + imports: [ProductModuleBase, forwardRef(() => AuthModule)], + controllers: [ProductController], + providers: [ProductService, ProductResolver], + exports: [ProductService], +}) +export class ProductModule {} diff --git a/apps/order-service/src/product/product.resolver.ts b/apps/order-service/src/product/product.resolver.ts new file mode 100644 index 0000000..6a2afd0 --- /dev/null +++ b/apps/order-service/src/product/product.resolver.ts @@ -0,0 +1,20 @@ +import * as graphql from "@nestjs/graphql"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { ProductResolverBase } from "./base/product.resolver.base"; +import { Product } from "./base/Product"; +import { ProductService } from "./product.service"; + +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => Product) +export class ProductResolver extends ProductResolverBase { + constructor( + protected readonly service: ProductService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/product/product.service.ts b/apps/order-service/src/product/product.service.ts new file mode 100644 index 0000000..9be8d4b --- /dev/null +++ b/apps/order-service/src/product/product.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { ProductServiceBase } from "./base/product.service.base"; + +@Injectable() +export class ProductService extends ProductServiceBase { + constructor(protected readonly prisma: PrismaService) { + super(prisma); + } +} diff --git a/apps/order-service/src/providers/secrets/base/secretsManager.service.base.spec.ts b/apps/order-service/src/providers/secrets/base/secretsManager.service.base.spec.ts new file mode 100644 index 0000000..f161172 --- /dev/null +++ b/apps/order-service/src/providers/secrets/base/secretsManager.service.base.spec.ts @@ -0,0 +1,41 @@ +import { ConfigService } from "@nestjs/config"; +import { mock } from "jest-mock-extended"; +import { SecretsManagerServiceBase } from "./secretsManager.service.base"; +import { EnumSecretsNameKey } from "../secretsNameKey.enum"; + +describe("Testing the secrets manager base class", () => { + const SECRET_KEY = "SECRET_KEY"; + const SECRET_VALUE = "SECRET_VALUE"; + const configService = mock<ConfigService>(); + const secretsManagerServiceBase = new SecretsManagerServiceBase( + configService + ); + beforeEach(() => { + configService.get.mockClear(); + }); + it("should return value from env", async () => { + //ARRANGE + configService.get.mockReturnValue(SECRET_VALUE); + //ACT + const result = await secretsManagerServiceBase.getSecret( + SECRET_KEY as unknown as EnumSecretsNameKey + ); + //ASSERT + expect(result).toBe(SECRET_VALUE); + }); + it("should return null for unknown keys", async () => { + //ARRANGE + configService.get.mockReturnValue(undefined); + //ACT + const result = await secretsManagerServiceBase.getSecret( + SECRET_KEY as unknown as EnumSecretsNameKey + ); + //ASSERT + expect(result).toBeNull(); + }); + it("should throw an exception if getting null key", () => { + return expect( + secretsManagerServiceBase.getSecret(null as unknown as EnumSecretsNameKey) + ).rejects.toThrow(); + }); +}); diff --git a/apps/order-service/src/providers/secrets/base/secretsManager.service.base.ts b/apps/order-service/src/providers/secrets/base/secretsManager.service.base.ts new file mode 100644 index 0000000..340818c --- /dev/null +++ b/apps/order-service/src/providers/secrets/base/secretsManager.service.base.ts @@ -0,0 +1,17 @@ +import { ConfigService } from "@nestjs/config"; +import { EnumSecretsNameKey } from "../secretsNameKey.enum"; + +export interface ISecretsManager { + getSecret: (key: EnumSecretsNameKey) => Promise<any | null>; +} + +export class SecretsManagerServiceBase implements ISecretsManager { + constructor(protected readonly configService: ConfigService) {} + async getSecret<T>(key: EnumSecretsNameKey): Promise<T | null> { + const value = this.configService.get(key.toString()); + if (value) { + return value; + } + return null; + } +} diff --git a/apps/order-service/src/providers/secrets/secretsManager.module.ts b/apps/order-service/src/providers/secrets/secretsManager.module.ts new file mode 100644 index 0000000..3a621e4 --- /dev/null +++ b/apps/order-service/src/providers/secrets/secretsManager.module.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { SecretsManagerService } from "./secretsManager.service"; + +@Module({ + providers: [SecretsManagerService], + exports: [SecretsManagerService], +}) +export class SecretsManagerModule {} diff --git a/apps/order-service/src/providers/secrets/secretsManager.service.ts b/apps/order-service/src/providers/secrets/secretsManager.service.ts new file mode 100644 index 0000000..89907c3 --- /dev/null +++ b/apps/order-service/src/providers/secrets/secretsManager.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { SecretsManagerServiceBase } from "./base/secretsManager.service.base"; + +@Injectable() +export class SecretsManagerService extends SecretsManagerServiceBase { + constructor(protected readonly configService: ConfigService) { + super(configService); + } +} diff --git a/apps/order-service/src/providers/secrets/secretsNameKey.enum.ts b/apps/order-service/src/providers/secrets/secretsNameKey.enum.ts new file mode 100644 index 0000000..52c18af --- /dev/null +++ b/apps/order-service/src/providers/secrets/secretsNameKey.enum.ts @@ -0,0 +1,3 @@ +export enum EnumSecretsNameKey { + JwtSecretKey = "JWT_SECRET_KEY" +} \ No newline at end of file diff --git a/apps/order-service/src/serveStaticOptions.service.ts b/apps/order-service/src/serveStaticOptions.service.ts new file mode 100644 index 0000000..390248b --- /dev/null +++ b/apps/order-service/src/serveStaticOptions.service.ts @@ -0,0 +1,39 @@ +import * as path from "path"; +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { + ServeStaticModuleOptions, + ServeStaticModuleOptionsFactory, +} from "@nestjs/serve-static"; + +const SERVE_STATIC_ROOT_PATH_VAR = "SERVE_STATIC_ROOT_PATH"; +const DEFAULT_STATIC_MODULE_OPTIONS_LIST: ServeStaticModuleOptions[] = [ + { + serveRoot: "/swagger", + rootPath: path.join(__dirname, "swagger"), + }, +]; + +@Injectable() +export class ServeStaticOptionsService + implements ServeStaticModuleOptionsFactory +{ + private readonly logger = new Logger(ServeStaticOptionsService.name); + + constructor(private readonly configService: ConfigService) {} + + createLoggerOptions(): ServeStaticModuleOptions[] { + const serveStaticRootPath = this.configService.get( + SERVE_STATIC_ROOT_PATH_VAR + ); + if (serveStaticRootPath) { + const resolvedPath = path.resolve(serveStaticRootPath); + this.logger.log(`Serving static files from ${resolvedPath}`); + return [ + ...DEFAULT_STATIC_MODULE_OPTIONS_LIST, + { rootPath: resolvedPath, exclude: ["/api*", "/graphql"] }, + ]; + } + return DEFAULT_STATIC_MODULE_OPTIONS_LIST; + } +} diff --git a/apps/order-service/src/swagger.ts b/apps/order-service/src/swagger.ts new file mode 100644 index 0000000..d5046e4 --- /dev/null +++ b/apps/order-service/src/swagger.ts @@ -0,0 +1,20 @@ +import { DocumentBuilder, SwaggerCustomOptions } from "@nestjs/swagger"; + +export const swaggerPath = "api"; + +export const swaggerDocumentOptions = new DocumentBuilder() + .setTitle("Order Service") + .setDescription( + 'Sample service for e-commerce\n\n## Congratulations! Your service resource is ready.\n \nPlease note that all endpoints are secured with JWT Bearer authentication.\nBy default, your service resource comes with one user with the username "admin" and password "admin".\nLearn more in [our docs](https://docs.amplication.com)' + ) + .addBearerAuth() + .build(); + +export const swaggerSetupOptions: SwaggerCustomOptions = { + swaggerOptions: { + persistAuthorization: true, + }, + customCssUrl: "../swagger/swagger.css", + customfavIcon: "../swagger/favicon.png", + customSiteTitle: "Order Service", +}; diff --git a/apps/order-service/src/swagger/favicon.png b/apps/order-service/src/swagger/favicon.png new file mode 100644 index 0000000..a79882d Binary files /dev/null and b/apps/order-service/src/swagger/favicon.png differ diff --git a/apps/order-service/src/swagger/logo-amplication-white.svg b/apps/order-service/src/swagger/logo-amplication-white.svg new file mode 100644 index 0000000..0054cd4 --- /dev/null +++ b/apps/order-service/src/swagger/logo-amplication-white.svg @@ -0,0 +1,15 @@ +<svg width="206" height="45" viewBox="0 0 206 45" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 22.472C0 34.0994 8.90877 43.6025 20.3874 44.9441V0C8.90877 1.34161 0 10.9006 0 22.472Z" fill="white"/> +<path d="M46.2574 22.472C46.2574 10.8447 37.2344 1.22981 25.6987 0V45H46.2574V23.1429C46.2574 22.9193 46.2574 22.6957 46.2574 22.472Z" fill="white"/> +<path d="M70.8698 26.2732V38.8508H67.6718V37.1738C66.8152 38.5154 64.9877 39.1303 63.3887 39.1303C60.0194 39.1303 57.0498 36.5589 57.0498 32.534C57.0498 28.5092 59.9623 25.9937 63.3887 25.9937C64.9877 25.9937 66.8152 26.6086 67.6718 27.9502V26.2732H70.8698ZM67.6147 32.534C67.6147 30.298 65.7301 28.9005 63.9027 28.9005C61.9039 28.9005 60.3049 30.4098 60.3049 32.534C60.3049 34.6583 61.9039 36.2235 63.9027 36.2235C65.8444 36.2235 67.6147 34.7701 67.6147 32.534Z" fill="white"/> +<path d="M95.14 31.8074V38.8509H91.942V32.031C91.942 30.0186 90.857 29.0124 89.3722 29.0124C87.5447 29.0124 86.3455 30.354 86.5168 32.6459V32.5341V38.8509H83.3188V32.031C83.3188 30.0186 82.2337 29.0124 80.8632 29.0124C79.3784 29.0124 77.8936 29.795 77.8936 32.1987V38.8509H74.6956V26.2733H77.8936V28.4534C78.3504 26.8882 80.1208 26.0497 81.7198 26.0497C83.6043 26.0497 85.1462 26.8882 85.8886 28.5093C86.9166 26.4969 88.9153 26.0497 90.2288 26.0497C93.4268 25.9938 95.14 28.118 95.14 31.8074Z" fill="white"/> +<path d="M112.614 32.534C112.614 36.5589 109.702 39.0744 106.276 39.0744C104.677 39.0744 102.849 38.4595 101.992 37.1179V44.944H98.7944V26.2732H101.992V27.9502C102.849 26.6086 104.734 25.9937 106.276 25.9937C109.645 25.9937 112.614 28.5651 112.614 32.534ZM109.416 32.5899C109.416 30.4657 107.76 28.9005 105.762 28.9005C103.82 28.9005 102.05 30.3539 102.05 32.5899C102.05 34.8259 103.934 36.2235 105.762 36.2235C107.76 36.2235 109.416 34.7141 109.416 32.5899Z" fill="white"/> +<path d="M115.642 19.5092H118.84V38.8508H115.642V19.5092Z" fill="white"/> +<path d="M122.152 21.8006C122.152 20.6826 123.123 19.9 124.265 19.9C125.407 19.9 126.321 20.6826 126.321 21.8006C126.321 22.8628 125.407 23.7013 124.265 23.7013C123.123 23.7572 122.152 22.9187 122.152 21.8006ZM122.609 26.2727H125.807V38.8503H122.609V26.2727Z" fill="white"/> +<path d="M128.834 32.534C128.834 28.5651 132.09 25.9937 135.859 25.9937C138.086 25.9937 139.913 26.944 141.113 28.3974L138.828 30.1303C138.143 29.3477 137.058 28.8446 135.916 28.8446C133.689 28.8446 132.09 30.3539 132.09 32.4781C132.09 34.6024 133.689 36.1117 135.916 36.1117C137.058 36.1117 138.143 35.6086 138.828 34.826L141.113 36.5589C139.913 38.0123 138.086 38.9626 135.859 38.9626C132.09 39.1303 128.834 36.5589 128.834 32.534Z" fill="white"/> +<path d="M156.589 26.2732V38.8508H153.391V37.1738C152.534 38.5154 150.707 39.1303 149.107 39.1303C145.738 39.1303 142.769 36.5589 142.769 32.534C142.769 28.5092 145.681 25.9937 149.107 25.9937C150.707 25.9937 152.534 26.6086 153.391 27.9502V26.2732H156.589ZM153.333 32.534C153.333 30.298 151.449 28.9005 149.621 28.9005C147.623 28.9005 146.024 30.4098 146.024 32.534C146.024 34.6583 147.623 36.2235 149.621 36.2235C151.563 36.2235 153.333 34.7701 153.333 32.534Z" fill="white"/> +<path d="M167.496 28.7891H164.755V38.9071H161.556V28.7891H159.215V26.2735H161.556V21.6338H164.755V26.2735H167.496V28.7891Z" fill="white"/> +<path d="M169.438 21.8006C169.438 20.6826 170.409 19.9 171.551 19.9C172.693 19.9 173.607 20.6826 173.607 21.8006C173.607 22.8628 172.693 23.7013 171.551 23.7013C170.409 23.7572 169.438 22.9187 169.438 21.8006ZM169.952 26.2727H173.15V38.8503H169.952V26.2727Z" fill="white"/> +<path d="M176.12 32.534C176.12 28.5651 179.318 25.9937 182.973 25.9937C186.627 25.9937 189.825 28.5092 189.825 32.534C189.825 36.503 186.627 39.1303 182.973 39.1303C179.318 39.1303 176.12 36.503 176.12 32.534ZM186.627 32.534C186.627 30.3539 184.971 28.9005 182.973 28.9005C180.974 28.9005 179.375 30.3539 179.375 32.534C179.375 34.7701 180.974 36.2235 182.973 36.2235C184.971 36.2235 186.627 34.7701 186.627 32.534Z" fill="white"/> +<path d="M205.187 31.8073V38.8508H201.989V32.0868C201.989 30.0744 200.733 29.0682 199.305 29.0682C197.82 29.0682 196.05 29.8508 196.05 32.2545V38.8508H192.852V26.2732H196.05V28.2856C196.735 26.7204 198.734 25.9937 200.047 25.9937C203.36 25.9937 205.187 28.1179 205.187 31.8073Z" fill="white"/> +</svg> diff --git a/apps/order-service/src/swagger/swagger.css b/apps/order-service/src/swagger/swagger.css new file mode 100644 index 0000000..b7c4037 --- /dev/null +++ b/apps/order-service/src/swagger/swagger.css @@ -0,0 +1,321 @@ +html, +body { + background-color: #f4f4f7; +} + +body { + margin: auto; + line-height: 1.6; + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + color: #121242; +} + +.swagger-ui { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; +} + +.swagger-ui button, +.swagger-ui input, +.swagger-ui optgroup, +.swagger-ui select, +.swagger-ui textarea, +.swagger-ui .parameter__name, +.swagger-ui .parameters-col_name > *, +.swagger-ui label { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: normal; + font-size: 12px; + outline: none; +} + +.swagger-ui textarea { + border: 1px solid #d0d0d9; + min-height: 100px; +} + +.swagger-ui input[type="email"], +.swagger-ui input[type="file"], +.swagger-ui input[type="password"], +.swagger-ui input[type="search"], +.swagger-ui input[type="text"], +.swagger-ui textarea { + border-radius: 3px; +} + +.swagger-ui input[disabled], +.swagger-ui select[disabled], +.swagger-ui textarea[disabled] { + background: #f4f4f7; + color: #b8b8c6; +} + +.swagger-ui .btn { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: 500; + box-shadow: none; + border: 1px solid #d0d0d9; + height: 28px; + border-radius: 14px; + background-color: #fff; + color: #7950ed; +} + +.swagger-ui .btn:hover { + box-shadow: none; +} + +/* topbar */ + +.swagger-ui .topbar { + background-color: #7950ed; + height: 80px; + padding: 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; +} + +.swagger-ui .topbar-wrapper a { + display: block; + width: 206px; + height: 35px; + background-image: url("logo-amplication-white.svg"); + background-repeat: no-repeat; + background-size: contain; +} + +.swagger-ui .topbar-wrapper svg, +.swagger-ui .topbar-wrapper img { + display: none; +} + +/* title */ +.swagger-ui .info { + margin: 0; +} + +.swagger-ui .info .title { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-size: 32px; + font-weight: 600; +} + +.swagger-ui .information-container { + padding-top: 50px; + padding-bottom: 20px; + position: relative; +} + +.swagger-ui .info .title small.version-stamp { + display: none; +} + +.swagger-ui .info .title small { + background-color: #a787ff; +} + +.swagger-ui .info .description p { + max-width: 1000px; + margin: 0; +} + +.swagger-ui .info .description p, +.swagger-ui .info .description a { + font-size: 1rem; +} + +.swagger-ui .information-container section { + position: relative; +} + +.swagger-ui .scheme-container { + box-shadow: none; + background-color: transparent; + position: relative; + margin: 0; + margin-top: 20px; + margin-bottom: 20px; + padding: 0; +} + +.swagger-ui .scheme-container .auth-wrapper { + justify-content: flex-start; +} + +.swagger-ui .btn.authorize { + box-shadow: none; + border: 1px solid #d0d0d9; + height: 40px; + border-radius: 20px; + background-color: #fff; + color: #7950ed; +} + +.swagger-ui .btn.authorize svg { + fill: #7950ed; +} + +/* content */ + +.swagger-ui .opblock-tag { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-size: 28px; + font-weight: 600; +} + +.swagger-ui .opblock.is-open .opblock-summary { + border-color: #e7e7ec !important; + border-bottom: none; +} + +.swagger-ui .opblock .opblock-section-header { + background-color: #fff; + border: none; + border-top: 1px solid #e7e7ec; + border-bottom: 1px solid #e7e7ec; + box-shadow: none; +} + +.swagger-ui .opblock .tab-header .tab-item.active h4 span:after { + display: none; +} + +.swagger-ui .opblock.opblock-post { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; + color: #fff; +} + +.swagger-ui .opblock.opblock-post:hover, +.swagger-ui .opblock.opblock-post.is-open { + border-color: #31c587; +} + +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background-color: #31c587; +} + +.swagger-ui .opblock.opblock-get { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; +} +.swagger-ui .opblock.opblock-get:hover, +.swagger-ui .opblock.opblock-get.is-open { + border-color: #20a4f3; +} +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background-color: #20a4f3; +} + +.swagger-ui .opblock.opblock-delete { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; +} +.swagger-ui .opblock.opblock-delete:hover, +.swagger-ui .opblock.opblock-delete.is-open { + border-color: #e93c51; +} +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background-color: #e93c51; +} + +.swagger-ui .opblock.opblock-patch { + border: 1px solid #e7e7ec; + background: #f9f9fa; + box-shadow: none; +} +.swagger-ui .opblock.opblock-patch:hover, +.swagger-ui .opblock.opblock-patch.is-open { + border-color: #41cadd; +} +.swagger-ui .opblock.opblock-patch .opblock-summary-method { + background-color: #41cadd; +} + +.swagger-ui .opblock-body pre { + background-color: #121242 !important; +} + +.swagger-ui select, +.swagger-ui .response-control-media-type--accept-controller select { + border: 1px solid #d0d0d9; + box-shadow: none; + outline: none; +} + +/* models */ + +.swagger-ui section.models { + background-color: #fff; + border: 1px solid #e7e7ec; +} + +.swagger-ui section.models.is-open h4 { + border-bottom: 1px solid #e7e7ec; +} + +.swagger-ui section.models .model-container, +.swagger-ui section.models .model-container:hover { + background-color: #f4f4f7; + color: #121242; +} + +.swagger-ui .model-title { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: normal; + font-size: 15px; + color: #121242; +} + +/* modal */ + +.swagger-ui .dialog-ux .modal-ux-header h3, +.swagger-ui .dialog-ux .modal-ux-content h4, +.swagger-ui .dialog-ux .modal-ux-content h4 code { + font-family: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", + "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", + "Helvetica Neue", sans-serif; + font-weight: normal; + font-size: 15px; + color: #121242; +} + +.swagger-ui .dialog-ux .modal-ux-content .btn.authorize { + height: 28px; + border-radius: 14px; +} + +.swagger-ui .auth-btn-wrapper { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: flex-start; +} + +.swagger-ui .auth-btn-wrapper .btn-done { + border: none; + color: #121242; + margin-right: 0; +} + +.swagger-ui .authorization__btn { + fill: #414168; +} diff --git a/apps/order-service/src/tests/auth/constants.ts b/apps/order-service/src/tests/auth/constants.ts new file mode 100644 index 0000000..98e4887 --- /dev/null +++ b/apps/order-service/src/tests/auth/constants.ts @@ -0,0 +1,19 @@ +import { Credentials } from "../../auth/Credentials"; +import { UserInfo } from "../../auth/UserInfo"; + +export const VALID_ID = "1"; + +export const TEST_USER: UserInfo = { + id: "cl7qmjh4h0000tothyjqapgj5", + roles: ["User"], + username: "ofek", +}; +export const SIGN_TOKEN = "SIGN_TOKEN"; +export const VALID_CREDENTIALS: Credentials = { + username: "Valid User", + password: "Valid User Password", +}; +export const INVALID_CREDENTIALS: Credentials = { + username: "Invalid User", + password: "Invalid User Password", +}; diff --git a/apps/order-service/src/tests/auth/jwt/jwt.strategy.spec.ts b/apps/order-service/src/tests/auth/jwt/jwt.strategy.spec.ts new file mode 100644 index 0000000..36bbb24 --- /dev/null +++ b/apps/order-service/src/tests/auth/jwt/jwt.strategy.spec.ts @@ -0,0 +1,28 @@ +import { UnauthorizedException } from "@nestjs/common"; +import { mock } from "jest-mock-extended"; +import { JwtStrategyBase } from "../../../auth/jwt/base/jwt.strategy.base"; +import { TEST_USER } from "../constants"; +import { UserService } from "../../../user/user.service"; +describe("Testing the jwtStrategyBase.validate()", () => { + const userService = mock<UserService>(); + const jwtStrategy = new JwtStrategyBase("Secrete", userService); + beforeEach(() => { + userService.user.mockClear(); + }); + it("should throw UnauthorizedException where there is no user", async () => { + //ARRANGE + userService.user + .calledWith({ + where: { username: TEST_USER.username }, + }) + .mockReturnValue(Promise.resolve(null)); + //ACT + const result = jwtStrategy.validate({ + id: TEST_USER.id, + username: TEST_USER.username, + roles: TEST_USER.roles, + }); + //ASSERT + return expect(result).rejects.toThrowError(UnauthorizedException); + }); +}); diff --git a/apps/order-service/src/tests/auth/token.service.spec.ts b/apps/order-service/src/tests/auth/token.service.spec.ts new file mode 100644 index 0000000..83b5a51 --- /dev/null +++ b/apps/order-service/src/tests/auth/token.service.spec.ts @@ -0,0 +1,47 @@ +import { JwtService } from "@nestjs/jwt"; +import { mock } from "jest-mock-extended"; +import { TokenServiceBase } from "../../auth/base/token.service.base"; +import { + INVALID_PASSWORD_ERROR, + INVALID_USERNAME_ERROR, +} from "../../auth/constants"; +import { SIGN_TOKEN, VALID_CREDENTIALS, VALID_ID } from "./constants"; + +describe("Testing the TokenServiceBase", () => { + let tokenServiceBase: TokenServiceBase; + const jwtService = mock<JwtService>(); + beforeEach(() => { + tokenServiceBase = new TokenServiceBase(jwtService); + jwtService.signAsync.mockClear(); + }); + describe("Testing the BasicTokenService.createToken()", () => { + it("should create valid token for valid username and password", async () => { + jwtService.signAsync.mockReturnValue(Promise.resolve(SIGN_TOKEN)); + expect( + await tokenServiceBase.createToken({ + id: VALID_ID, + username: VALID_CREDENTIALS.username, + password: VALID_CREDENTIALS.password, + }) + ).toBe(SIGN_TOKEN); + }); + it("should reject when username missing", () => { + const result = tokenServiceBase.createToken({ + id: VALID_ID, + //@ts-ignore + username: null, + password: VALID_CREDENTIALS.password, + }); + return expect(result).rejects.toBe(INVALID_USERNAME_ERROR); + }); + it("should reject when password missing", () => { + const result = tokenServiceBase.createToken({ + id: VALID_ID, + username: VALID_CREDENTIALS.username, + //@ts-ignore + password: null, + }); + return expect(result).rejects.toBe(INVALID_PASSWORD_ERROR); + }); + }); +}); diff --git a/apps/order-service/src/tests/health/health.service.spec.ts b/apps/order-service/src/tests/health/health.service.spec.ts new file mode 100644 index 0000000..4b191f1 --- /dev/null +++ b/apps/order-service/src/tests/health/health.service.spec.ts @@ -0,0 +1,36 @@ +import { mock } from "jest-mock-extended"; +import { PrismaService } from "../../prisma/prisma.service"; +import { HealthServiceBase } from "../../health/base/health.service.base"; + +describe("Testing the HealthServiceBase", () => { + //ARRANGE + let prismaService: PrismaService; + let healthServiceBase: HealthServiceBase; + + describe("Testing the isDbReady function in HealthServiceBase class", () => { + beforeEach(() => { + prismaService = mock<PrismaService>(); + healthServiceBase = new HealthServiceBase(prismaService); + }); + it("should return true if allow connection to db", async () => { + //ARRANGE + (prismaService.$queryRaw as jest.Mock).mockReturnValue( + Promise.resolve(true) + ); + //ACT + const response = await healthServiceBase.isDbReady(); + //ASSERT + expect(response).toBe(true); + }); + it("should return false if db is not available", async () => { + //ARRANGE + (prismaService.$queryRaw as jest.Mock).mockReturnValue( + Promise.reject(false) + ); + //ACT + const response = await healthServiceBase.isDbReady(); + //ASSERT + expect(response).toBe(false); + }); + }); +}); diff --git a/apps/order-service/src/types.ts b/apps/order-service/src/types.ts new file mode 100644 index 0000000..f762a5d --- /dev/null +++ b/apps/order-service/src/types.ts @@ -0,0 +1,3 @@ +import type { JsonValue } from "type-fest"; + +export type InputJsonValue = Omit<JsonValue, "null">; diff --git a/apps/order-service/src/user/base/CreateUserArgs.ts b/apps/order-service/src/user/base/CreateUserArgs.ts new file mode 100644 index 0000000..092a504 --- /dev/null +++ b/apps/order-service/src/user/base/CreateUserArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { UserCreateInput } from "./UserCreateInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class CreateUserArgs { + @ApiProperty({ + required: true, + type: () => UserCreateInput, + }) + @ValidateNested() + @Type(() => UserCreateInput) + @Field(() => UserCreateInput, { nullable: false }) + data!: UserCreateInput; +} + +export { CreateUserArgs as CreateUserArgs }; diff --git a/apps/order-service/src/user/base/DeleteUserArgs.ts b/apps/order-service/src/user/base/DeleteUserArgs.ts new file mode 100644 index 0000000..042ed5f --- /dev/null +++ b/apps/order-service/src/user/base/DeleteUserArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { UserWhereUniqueInput } from "./UserWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class DeleteUserArgs { + @ApiProperty({ + required: true, + type: () => UserWhereUniqueInput, + }) + @ValidateNested() + @Type(() => UserWhereUniqueInput) + @Field(() => UserWhereUniqueInput, { nullable: false }) + where!: UserWhereUniqueInput; +} + +export { DeleteUserArgs as DeleteUserArgs }; diff --git a/apps/order-service/src/user/base/UpdateUserArgs.ts b/apps/order-service/src/user/base/UpdateUserArgs.ts new file mode 100644 index 0000000..1f13420 --- /dev/null +++ b/apps/order-service/src/user/base/UpdateUserArgs.ts @@ -0,0 +1,40 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { UserWhereUniqueInput } from "./UserWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; +import { UserUpdateInput } from "./UserUpdateInput"; + +@ArgsType() +class UpdateUserArgs { + @ApiProperty({ + required: true, + type: () => UserWhereUniqueInput, + }) + @ValidateNested() + @Type(() => UserWhereUniqueInput) + @Field(() => UserWhereUniqueInput, { nullable: false }) + where!: UserWhereUniqueInput; + + @ApiProperty({ + required: true, + type: () => UserUpdateInput, + }) + @ValidateNested() + @Type(() => UserUpdateInput) + @Field(() => UserUpdateInput, { nullable: false }) + data!: UserUpdateInput; +} + +export { UpdateUserArgs as UpdateUserArgs }; diff --git a/apps/order-service/src/user/base/User.ts b/apps/order-service/src/user/base/User.ts new file mode 100644 index 0000000..348fd32 --- /dev/null +++ b/apps/order-service/src/user/base/User.ts @@ -0,0 +1,86 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsDate, IsString, MaxLength, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +import { IsJSONValue } from "../../validators"; +import { GraphQLJSON } from "graphql-type-json"; +import { JsonValue } from "type-fest"; + +@ObjectType() +class User { + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + createdAt!: Date; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(256) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + firstName!: string | null; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(256) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + lastName!: string | null; + + @ApiProperty({ + required: true, + }) + @IsJSONValue() + @Field(() => GraphQLJSON) + roles!: JsonValue; + + @ApiProperty({ + required: true, + }) + @IsDate() + @Type(() => Date) + @Field(() => Date) + updatedAt!: Date; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + username!: string; +} + +export { User as User }; diff --git a/apps/order-service/src/user/base/UserCountArgs.ts b/apps/order-service/src/user/base/UserCountArgs.ts new file mode 100644 index 0000000..f4a14ac --- /dev/null +++ b/apps/order-service/src/user/base/UserCountArgs.ts @@ -0,0 +1,28 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { UserWhereInput } from "./UserWhereInput"; +import { Type } from "class-transformer"; + +@ArgsType() +class UserCountArgs { + @ApiProperty({ + required: false, + type: () => UserWhereInput, + }) + @Field(() => UserWhereInput, { nullable: true }) + @Type(() => UserWhereInput) + where?: UserWhereInput; +} + +export { UserCountArgs as UserCountArgs }; diff --git a/apps/order-service/src/user/base/UserCreateInput.ts b/apps/order-service/src/user/base/UserCreateInput.ts new file mode 100644 index 0000000..a98430e --- /dev/null +++ b/apps/order-service/src/user/base/UserCreateInput.ts @@ -0,0 +1,69 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, MaxLength, IsOptional } from "class-validator"; +import { IsJSONValue } from "../../validators"; +import { GraphQLJSON } from "graphql-type-json"; +import { InputJsonValue } from "../../types"; + +@InputType() +class UserCreateInput { + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(256) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + firstName?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(256) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + lastName?: string | null; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + password!: string; + + @ApiProperty({ + required: true, + }) + @IsJSONValue() + @Field(() => GraphQLJSON) + roles!: InputJsonValue; + + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + username!: string; +} + +export { UserCreateInput as UserCreateInput }; diff --git a/apps/order-service/src/user/base/UserFindManyArgs.ts b/apps/order-service/src/user/base/UserFindManyArgs.ts new file mode 100644 index 0000000..8ba369c --- /dev/null +++ b/apps/order-service/src/user/base/UserFindManyArgs.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { UserWhereInput } from "./UserWhereInput"; +import { IsOptional, ValidateNested, IsInt } from "class-validator"; +import { Type } from "class-transformer"; +import { UserOrderByInput } from "./UserOrderByInput"; + +@ArgsType() +class UserFindManyArgs { + @ApiProperty({ + required: false, + type: () => UserWhereInput, + }) + @IsOptional() + @ValidateNested() + @Field(() => UserWhereInput, { nullable: true }) + @Type(() => UserWhereInput) + where?: UserWhereInput; + + @ApiProperty({ + required: false, + type: [UserOrderByInput], + }) + @IsOptional() + @ValidateNested({ each: true }) + @Field(() => [UserOrderByInput], { nullable: true }) + @Type(() => UserOrderByInput) + orderBy?: Array<UserOrderByInput>; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + skip?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @IsInt() + @Field(() => Number, { nullable: true }) + @Type(() => Number) + take?: number; +} + +export { UserFindManyArgs as UserFindManyArgs }; diff --git a/apps/order-service/src/user/base/UserFindUniqueArgs.ts b/apps/order-service/src/user/base/UserFindUniqueArgs.ts new file mode 100644 index 0000000..c64506f --- /dev/null +++ b/apps/order-service/src/user/base/UserFindUniqueArgs.ts @@ -0,0 +1,30 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { ArgsType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { UserWhereUniqueInput } from "./UserWhereUniqueInput"; +import { ValidateNested } from "class-validator"; +import { Type } from "class-transformer"; + +@ArgsType() +class UserFindUniqueArgs { + @ApiProperty({ + required: true, + type: () => UserWhereUniqueInput, + }) + @ValidateNested() + @Type(() => UserWhereUniqueInput) + @Field(() => UserWhereUniqueInput, { nullable: false }) + where!: UserWhereUniqueInput; +} + +export { UserFindUniqueArgs as UserFindUniqueArgs }; diff --git a/apps/order-service/src/user/base/UserListRelationFilter.ts b/apps/order-service/src/user/base/UserListRelationFilter.ts new file mode 100644 index 0000000..0ff7d94 --- /dev/null +++ b/apps/order-service/src/user/base/UserListRelationFilter.ts @@ -0,0 +1,56 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { UserWhereInput } from "./UserWhereInput"; +import { ValidateNested, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType() +class UserListRelationFilter { + @ApiProperty({ + required: false, + type: () => UserWhereInput, + }) + @ValidateNested() + @Type(() => UserWhereInput) + @IsOptional() + @Field(() => UserWhereInput, { + nullable: true, + }) + every?: UserWhereInput; + + @ApiProperty({ + required: false, + type: () => UserWhereInput, + }) + @ValidateNested() + @Type(() => UserWhereInput) + @IsOptional() + @Field(() => UserWhereInput, { + nullable: true, + }) + some?: UserWhereInput; + + @ApiProperty({ + required: false, + type: () => UserWhereInput, + }) + @ValidateNested() + @Type(() => UserWhereInput) + @IsOptional() + @Field(() => UserWhereInput, { + nullable: true, + }) + none?: UserWhereInput; +} +export { UserListRelationFilter as UserListRelationFilter }; diff --git a/apps/order-service/src/user/base/UserOrderByInput.ts b/apps/order-service/src/user/base/UserOrderByInput.ts new file mode 100644 index 0000000..1ded2ed --- /dev/null +++ b/apps/order-service/src/user/base/UserOrderByInput.ts @@ -0,0 +1,111 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsEnum } from "class-validator"; +import { SortOrder } from "../../util/SortOrder"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +class UserOrderByInput { + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + createdAt?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + firstName?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + id?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + lastName?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + password?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + roles?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + updatedAt?: SortOrder; + + @ApiProperty({ + required: false, + enum: ["asc", "desc"], + }) + @IsOptional() + @IsEnum(SortOrder) + @Field(() => SortOrder, { + nullable: true, + }) + username?: SortOrder; +} + +export { UserOrderByInput as UserOrderByInput }; diff --git a/apps/order-service/src/user/base/UserUpdateInput.ts b/apps/order-service/src/user/base/UserUpdateInput.ts new file mode 100644 index 0000000..aefeb3f --- /dev/null +++ b/apps/order-service/src/user/base/UserUpdateInput.ts @@ -0,0 +1,78 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, MaxLength, IsOptional } from "class-validator"; +import { IsJSONValue } from "../../validators"; +import { GraphQLJSON } from "graphql-type-json"; +import { InputJsonValue } from "../../types"; + +@InputType() +class UserUpdateInput { + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(256) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + firstName?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @MaxLength(256) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + lastName?: string | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + password?: string; + + @ApiProperty({ + required: false, + }) + @IsJSONValue() + @IsOptional() + @Field(() => GraphQLJSON, { + nullable: true, + }) + roles?: InputJsonValue; + + @ApiProperty({ + required: false, + type: String, + }) + @IsString() + @IsOptional() + @Field(() => String, { + nullable: true, + }) + username?: string; +} + +export { UserUpdateInput as UserUpdateInput }; diff --git a/apps/order-service/src/user/base/UserWhereInput.ts b/apps/order-service/src/user/base/UserWhereInput.ts new file mode 100644 index 0000000..a8babc1 --- /dev/null +++ b/apps/order-service/src/user/base/UserWhereInput.ts @@ -0,0 +1,66 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { StringNullableFilter } from "../../util/StringNullableFilter"; +import { Type } from "class-transformer"; +import { IsOptional } from "class-validator"; +import { StringFilter } from "../../util/StringFilter"; + +@InputType() +class UserWhereInput { + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + firstName?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + id?: StringFilter; + + @ApiProperty({ + required: false, + type: StringNullableFilter, + }) + @Type(() => StringNullableFilter) + @IsOptional() + @Field(() => StringNullableFilter, { + nullable: true, + }) + lastName?: StringNullableFilter; + + @ApiProperty({ + required: false, + type: StringFilter, + }) + @Type(() => StringFilter) + @IsOptional() + @Field(() => StringFilter, { + nullable: true, + }) + username?: StringFilter; +} + +export { UserWhereInput as UserWhereInput }; diff --git a/apps/order-service/src/user/base/UserWhereUniqueInput.ts b/apps/order-service/src/user/base/UserWhereUniqueInput.ts new file mode 100644 index 0000000..0bcea12 --- /dev/null +++ b/apps/order-service/src/user/base/UserWhereUniqueInput.ts @@ -0,0 +1,27 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { InputType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +@InputType() +class UserWhereUniqueInput { + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @Field(() => String) + id!: string; +} + +export { UserWhereUniqueInput as UserWhereUniqueInput }; diff --git a/apps/order-service/src/user/base/user.controller.base.spec.ts b/apps/order-service/src/user/base/user.controller.base.spec.ts new file mode 100644 index 0000000..fb15280 --- /dev/null +++ b/apps/order-service/src/user/base/user.controller.base.spec.ts @@ -0,0 +1,206 @@ +import { Test } from "@nestjs/testing"; +import { + INestApplication, + HttpStatus, + ExecutionContext, + CallHandler, +} from "@nestjs/common"; +import request from "supertest"; +import { ACGuard } from "nest-access-control"; +import { DefaultAuthGuard } from "../../auth/defaultAuth.guard"; +import { ACLModule } from "../../auth/acl.module"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { map } from "rxjs"; +import { UserController } from "../user.controller"; +import { UserService } from "../user.service"; + +const nonExistingId = "nonExistingId"; +const existingId = "existingId"; +const CREATE_INPUT = { + createdAt: new Date(), + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + password: "examplePassword", + updatedAt: new Date(), + username: "exampleUsername", +}; +const CREATE_RESULT = { + createdAt: new Date(), + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + password: "examplePassword", + updatedAt: new Date(), + username: "exampleUsername", +}; +const FIND_MANY_RESULT = [ + { + createdAt: new Date(), + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + password: "examplePassword", + updatedAt: new Date(), + username: "exampleUsername", + }, +]; +const FIND_ONE_RESULT = { + createdAt: new Date(), + firstName: "exampleFirstName", + id: "exampleId", + lastName: "exampleLastName", + password: "examplePassword", + updatedAt: new Date(), + username: "exampleUsername", +}; + +const service = { + createUser() { + return CREATE_RESULT; + }, + users: () => FIND_MANY_RESULT, + user: ({ where }: { where: { id: string } }) => { + switch (where.id) { + case existingId: + return FIND_ONE_RESULT; + case nonExistingId: + return null; + } + }, +}; + +const basicAuthGuard = { + canActivate: (context: ExecutionContext) => { + const argumentHost = context.switchToHttp(); + const request = argumentHost.getRequest(); + request.user = { + roles: ["user"], + }; + return true; + }, +}; + +const acGuard = { + canActivate: () => { + return true; + }, +}; + +const aclFilterResponseInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle().pipe( + map((data) => { + return data; + }) + ); + }, +}; +const aclValidateRequestInterceptor = { + intercept: (context: ExecutionContext, next: CallHandler) => { + return next.handle(); + }, +}; + +describe("User", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: UserService, + useValue: service, + }, + ], + controllers: [UserController], + imports: [ACLModule], + }) + .overrideGuard(DefaultAuthGuard) + .useValue(basicAuthGuard) + .overrideGuard(ACGuard) + .useValue(acGuard) + .overrideInterceptor(AclFilterResponseInterceptor) + .useValue(aclFilterResponseInterceptor) + .overrideInterceptor(AclValidateRequestInterceptor) + .useValue(aclValidateRequestInterceptor) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + test("POST /users", async () => { + await request(app.getHttpServer()) + .post("/users") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }); + }); + + test("GET /users", async () => { + await request(app.getHttpServer()) + .get("/users") + .expect(HttpStatus.OK) + .expect([ + { + ...FIND_MANY_RESULT[0], + createdAt: FIND_MANY_RESULT[0].createdAt.toISOString(), + updatedAt: FIND_MANY_RESULT[0].updatedAt.toISOString(), + }, + ]); + }); + + test("GET /users/:id non existing", async () => { + await request(app.getHttpServer()) + .get(`${"/users"}/${nonExistingId}`) + .expect(HttpStatus.NOT_FOUND) + .expect({ + statusCode: HttpStatus.NOT_FOUND, + message: `No resource was found for {"${"id"}":"${nonExistingId}"}`, + error: "Not Found", + }); + }); + + test("GET /users/:id existing", async () => { + await request(app.getHttpServer()) + .get(`${"/users"}/${existingId}`) + .expect(HttpStatus.OK) + .expect({ + ...FIND_ONE_RESULT, + createdAt: FIND_ONE_RESULT.createdAt.toISOString(), + updatedAt: FIND_ONE_RESULT.updatedAt.toISOString(), + }); + }); + + test("POST /users existing resource", async () => { + const agent = request(app.getHttpServer()); + await agent + .post("/users") + .send(CREATE_INPUT) + .expect(HttpStatus.CREATED) + .expect({ + ...CREATE_RESULT, + createdAt: CREATE_RESULT.createdAt.toISOString(), + updatedAt: CREATE_RESULT.updatedAt.toISOString(), + }) + .then(function () { + agent + .post("/users") + .send(CREATE_INPUT) + .expect(HttpStatus.CONFLICT) + .expect({ + statusCode: HttpStatus.CONFLICT, + }); + }); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/apps/order-service/src/user/base/user.controller.base.ts b/apps/order-service/src/user/base/user.controller.base.ts new file mode 100644 index 0000000..57e9815 --- /dev/null +++ b/apps/order-service/src/user/base/user.controller.base.ts @@ -0,0 +1,202 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import { isRecordNotFoundError } from "../../prisma.util"; +import * as errors from "../../errors"; +import { Request } from "express"; +import { plainToClass } from "class-transformer"; +import { ApiNestedQuery } from "../../decorators/api-nested-query.decorator"; +import * as nestAccessControl from "nest-access-control"; +import * as defaultAuthGuard from "../../auth/defaultAuth.guard"; +import { UserService } from "../user.service"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { UserCreateInput } from "./UserCreateInput"; +import { User } from "./User"; +import { UserFindManyArgs } from "./UserFindManyArgs"; +import { UserWhereUniqueInput } from "./UserWhereUniqueInput"; +import { UserUpdateInput } from "./UserUpdateInput"; + +@swagger.ApiBearerAuth() +@common.UseGuards(defaultAuthGuard.DefaultAuthGuard, nestAccessControl.ACGuard) +export class UserControllerBase { + constructor( + protected readonly service: UserService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Post() + @swagger.ApiCreatedResponse({ type: User }) + @nestAccessControl.UseRoles({ + resource: "User", + action: "create", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async createUser(@common.Body() data: UserCreateInput): Promise<User> { + return await this.service.createUser({ + data: data, + select: { + createdAt: true, + firstName: true, + id: true, + lastName: true, + roles: true, + updatedAt: true, + username: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get() + @swagger.ApiOkResponse({ type: [User] }) + @ApiNestedQuery(UserFindManyArgs) + @nestAccessControl.UseRoles({ + resource: "User", + action: "read", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async users(@common.Req() request: Request): Promise<User[]> { + const args = plainToClass(UserFindManyArgs, request.query); + return this.service.users({ + ...args, + select: { + createdAt: true, + firstName: true, + id: true, + lastName: true, + roles: true, + updatedAt: true, + username: true, + }, + }); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @common.Get("/:id") + @swagger.ApiOkResponse({ type: User }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "User", + action: "read", + possession: "own", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async user( + @common.Param() params: UserWhereUniqueInput + ): Promise<User | null> { + const result = await this.service.user({ + where: params, + select: { + createdAt: true, + firstName: true, + id: true, + lastName: true, + roles: true, + updatedAt: true, + username: true, + }, + }); + if (result === null) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @common.Patch("/:id") + @swagger.ApiOkResponse({ type: User }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "User", + action: "update", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async updateUser( + @common.Param() params: UserWhereUniqueInput, + @common.Body() data: UserUpdateInput + ): Promise<User | null> { + try { + return await this.service.updateUser({ + where: params, + data: data, + select: { + createdAt: true, + firstName: true, + id: true, + lastName: true, + roles: true, + updatedAt: true, + username: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } + + @common.Delete("/:id") + @swagger.ApiOkResponse({ type: User }) + @swagger.ApiNotFoundResponse({ type: errors.NotFoundException }) + @nestAccessControl.UseRoles({ + resource: "User", + action: "delete", + possession: "any", + }) + @swagger.ApiForbiddenResponse({ + type: errors.ForbiddenException, + }) + async deleteUser( + @common.Param() params: UserWhereUniqueInput + ): Promise<User | null> { + try { + return await this.service.deleteUser({ + where: params, + select: { + createdAt: true, + firstName: true, + id: true, + lastName: true, + roles: true, + updatedAt: true, + username: true, + }, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new errors.NotFoundException( + `No resource was found for ${JSON.stringify(params)}` + ); + } + throw error; + } + } +} diff --git a/apps/order-service/src/user/base/user.module.base.ts b/apps/order-service/src/user/base/user.module.base.ts new file mode 100644 index 0000000..a8b6c68 --- /dev/null +++ b/apps/order-service/src/user/base/user.module.base.ts @@ -0,0 +1,18 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { Module } from "@nestjs/common"; +import { ACLModule } from "../../auth/acl.module"; +@Module({ + imports: [ACLModule], + exports: [ACLModule], +}) +export class UserModuleBase {} diff --git a/apps/order-service/src/user/base/user.resolver.base.ts b/apps/order-service/src/user/base/user.resolver.base.ts new file mode 100644 index 0000000..0c60d95 --- /dev/null +++ b/apps/order-service/src/user/base/user.resolver.base.ts @@ -0,0 +1,134 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import * as graphql from "@nestjs/graphql"; +import { GraphQLError } from "graphql"; +import { isRecordNotFoundError } from "../../prisma.util"; +import { MetaQueryPayload } from "../../util/MetaQueryPayload"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { AclFilterResponseInterceptor } from "../../interceptors/aclFilterResponse.interceptor"; +import { AclValidateRequestInterceptor } from "../../interceptors/aclValidateRequest.interceptor"; +import { User } from "./User"; +import { UserCountArgs } from "./UserCountArgs"; +import { UserFindManyArgs } from "./UserFindManyArgs"; +import { UserFindUniqueArgs } from "./UserFindUniqueArgs"; +import { CreateUserArgs } from "./CreateUserArgs"; +import { UpdateUserArgs } from "./UpdateUserArgs"; +import { DeleteUserArgs } from "./DeleteUserArgs"; +import { UserService } from "../user.service"; +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => User) +export class UserResolverBase { + constructor( + protected readonly service: UserService, + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) {} + + @graphql.Query(() => MetaQueryPayload) + @nestAccessControl.UseRoles({ + resource: "User", + action: "read", + possession: "any", + }) + async _usersMeta( + @graphql.Args() args: UserCountArgs + ): Promise<MetaQueryPayload> { + const result = await this.service.count(args); + return { + count: result, + }; + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => [User]) + @nestAccessControl.UseRoles({ + resource: "User", + action: "read", + possession: "any", + }) + async users(@graphql.Args() args: UserFindManyArgs): Promise<User[]> { + return this.service.users(args); + } + + @common.UseInterceptors(AclFilterResponseInterceptor) + @graphql.Query(() => User, { nullable: true }) + @nestAccessControl.UseRoles({ + resource: "User", + action: "read", + possession: "own", + }) + async user(@graphql.Args() args: UserFindUniqueArgs): Promise<User | null> { + const result = await this.service.user(args); + if (result === null) { + return null; + } + return result; + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => User) + @nestAccessControl.UseRoles({ + resource: "User", + action: "create", + possession: "any", + }) + async createUser(@graphql.Args() args: CreateUserArgs): Promise<User> { + return await this.service.createUser({ + ...args, + data: args.data, + }); + } + + @common.UseInterceptors(AclValidateRequestInterceptor) + @graphql.Mutation(() => User) + @nestAccessControl.UseRoles({ + resource: "User", + action: "update", + possession: "any", + }) + async updateUser(@graphql.Args() args: UpdateUserArgs): Promise<User | null> { + try { + return await this.service.updateUser({ + ...args, + data: args.data, + }); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } + + @graphql.Mutation(() => User) + @nestAccessControl.UseRoles({ + resource: "User", + action: "delete", + possession: "any", + }) + async deleteUser(@graphql.Args() args: DeleteUserArgs): Promise<User | null> { + try { + return await this.service.deleteUser(args); + } catch (error) { + if (isRecordNotFoundError(error)) { + throw new GraphQLError( + `No resource was found for ${JSON.stringify(args.where)}` + ); + } + throw error; + } + } +} diff --git a/apps/order-service/src/user/base/user.service.base.ts b/apps/order-service/src/user/base/user.service.base.ts new file mode 100644 index 0000000..6a749c5 --- /dev/null +++ b/apps/order-service/src/user/base/user.service.base.ts @@ -0,0 +1,62 @@ +/* +------------------------------------------------------------------------------ +This code was generated by Amplication. + +Changes to this file will be lost if the code is regenerated. + +There are other ways to to customize your code, see this doc to learn more +https://docs.amplication.com/how-to/custom-code + +------------------------------------------------------------------------------ + */ +import { PrismaService } from "../../prisma/prisma.service"; +import { Prisma, User as PrismaUser } from "@prisma/client"; +import { PasswordService } from "../../auth/password.service"; +import { transformStringFieldUpdateInput } from "../../prisma.util"; + +export class UserServiceBase { + constructor( + protected readonly prisma: PrismaService, + protected readonly passwordService: PasswordService + ) {} + + async count(args: Omit<Prisma.UserCountArgs, "select">): Promise<number> { + return this.prisma.user.count(args); + } + + async users(args: Prisma.UserFindManyArgs): Promise<PrismaUser[]> { + return this.prisma.user.findMany(args); + } + async user(args: Prisma.UserFindUniqueArgs): Promise<PrismaUser | null> { + return this.prisma.user.findUnique(args); + } + async createUser(args: Prisma.UserCreateArgs): Promise<PrismaUser> { + return this.prisma.user.create({ + ...args, + + data: { + ...args.data, + password: await this.passwordService.hash(args.data.password), + }, + }); + } + async updateUser(args: Prisma.UserUpdateArgs): Promise<PrismaUser> { + return this.prisma.user.update({ + ...args, + + data: { + ...args.data, + + password: + args.data.password && + (await transformStringFieldUpdateInput( + args.data.password, + (password) => this.passwordService.hash(password) + )), + }, + }); + } + async deleteUser(args: Prisma.UserDeleteArgs): Promise<PrismaUser> { + return this.prisma.user.delete(args); + } +} diff --git a/apps/order-service/src/user/user.controller.ts b/apps/order-service/src/user/user.controller.ts new file mode 100644 index 0000000..21d9583 --- /dev/null +++ b/apps/order-service/src/user/user.controller.ts @@ -0,0 +1,17 @@ +import * as common from "@nestjs/common"; +import * as swagger from "@nestjs/swagger"; +import * as nestAccessControl from "nest-access-control"; +import { UserService } from "./user.service"; +import { UserControllerBase } from "./base/user.controller.base"; + +@swagger.ApiTags("users") +@common.Controller("users") +export class UserController extends UserControllerBase { + constructor( + protected readonly service: UserService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/user/user.module.ts b/apps/order-service/src/user/user.module.ts new file mode 100644 index 0000000..2d6dd05 --- /dev/null +++ b/apps/order-service/src/user/user.module.ts @@ -0,0 +1,14 @@ +import { Module, forwardRef } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module"; +import { UserModuleBase } from "./base/user.module.base"; +import { UserService } from "./user.service"; +import { UserController } from "./user.controller"; +import { UserResolver } from "./user.resolver"; + +@Module({ + imports: [UserModuleBase, forwardRef(() => AuthModule)], + controllers: [UserController], + providers: [UserService, UserResolver], + exports: [UserService], +}) +export class UserModule {} diff --git a/apps/order-service/src/user/user.resolver.ts b/apps/order-service/src/user/user.resolver.ts new file mode 100644 index 0000000..bc3a79b --- /dev/null +++ b/apps/order-service/src/user/user.resolver.ts @@ -0,0 +1,20 @@ +import * as graphql from "@nestjs/graphql"; +import * as nestAccessControl from "nest-access-control"; +import * as gqlACGuard from "../auth/gqlAC.guard"; +import { GqlDefaultAuthGuard } from "../auth/gqlDefaultAuth.guard"; +import * as common from "@nestjs/common"; +import { UserResolverBase } from "./base/user.resolver.base"; +import { User } from "./base/User"; +import { UserService } from "./user.service"; + +@common.UseGuards(GqlDefaultAuthGuard, gqlACGuard.GqlACGuard) +@graphql.Resolver(() => User) +export class UserResolver extends UserResolverBase { + constructor( + protected readonly service: UserService, + @nestAccessControl.InjectRolesBuilder() + protected readonly rolesBuilder: nestAccessControl.RolesBuilder + ) { + super(service, rolesBuilder); + } +} diff --git a/apps/order-service/src/user/user.service.ts b/apps/order-service/src/user/user.service.ts new file mode 100644 index 0000000..a8e4508 --- /dev/null +++ b/apps/order-service/src/user/user.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { PasswordService } from "../auth/password.service"; +import { UserServiceBase } from "./base/user.service.base"; + +@Injectable() +export class UserService extends UserServiceBase { + constructor( + protected readonly prisma: PrismaService, + protected readonly passwordService: PasswordService + ) { + super(prisma, passwordService); + } +} diff --git a/apps/order-service/src/util/BooleanFilter.ts b/apps/order-service/src/util/BooleanFilter.ts new file mode 100644 index 0000000..75f4e34 --- /dev/null +++ b/apps/order-service/src/util/BooleanFilter.ts @@ -0,0 +1,32 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class BooleanFilter { + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + equals?: boolean; + + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + not?: boolean; +} diff --git a/apps/order-service/src/util/BooleanNullableFilter.ts b/apps/order-service/src/util/BooleanNullableFilter.ts new file mode 100644 index 0000000..9f48ac1 --- /dev/null +++ b/apps/order-service/src/util/BooleanNullableFilter.ts @@ -0,0 +1,31 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +@InputType({ + isAbstract: true, + description: undefined, +}) +export class BooleanNullableFilter { + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + equals?: boolean | null; + + @ApiProperty({ + required: false, + type: Boolean, + }) + @IsOptional() + @Field(() => Boolean, { + nullable: true, + }) + @Type(() => Boolean) + not?: boolean | null; +} diff --git a/apps/order-service/src/util/DateTimeFilter.ts b/apps/order-service/src/util/DateTimeFilter.ts new file mode 100644 index 0000000..d2b6dfb --- /dev/null +++ b/apps/order-service/src/util/DateTimeFilter.ts @@ -0,0 +1,97 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +@InputType({ + isAbstract: true, + description: undefined, +}) +export class DateTimeFilter { + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + equals?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + not?: Date; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + in?: Date[]; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + notIn?: Date[]; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lte?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gte?: Date; +} diff --git a/apps/order-service/src/util/DateTimeNullableFilter.ts b/apps/order-service/src/util/DateTimeNullableFilter.ts new file mode 100644 index 0000000..ccc00a5 --- /dev/null +++ b/apps/order-service/src/util/DateTimeNullableFilter.ts @@ -0,0 +1,97 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; +@InputType({ + isAbstract: true, + description: undefined, +}) +export class DateTimeNullableFilter { + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + equals?: Date | null; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + in?: Date[] | null; + + @ApiProperty({ + required: false, + type: [Date], + }) + @IsOptional() + @Field(() => [Date], { + nullable: true, + }) + @Type(() => Date) + notIn?: Date[] | null; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + lte?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gt?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + gte?: Date; + + @ApiProperty({ + required: false, + type: Date, + }) + @IsOptional() + @Field(() => Date, { + nullable: true, + }) + @Type(() => Date) + not?: Date; +} diff --git a/apps/order-service/src/util/FloatFilter.ts b/apps/order-service/src/util/FloatFilter.ts new file mode 100644 index 0000000..a3266d2 --- /dev/null +++ b/apps/order-service/src/util/FloatFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Float } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class FloatFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + equals?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + in?: number[]; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[]; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/apps/order-service/src/util/FloatNullableFilter.ts b/apps/order-service/src/util/FloatNullableFilter.ts new file mode 100644 index 0000000..feb0fc5 --- /dev/null +++ b/apps/order-service/src/util/FloatNullableFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Float } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class FloatNullableFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + equals?: number | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + in?: number[] | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Float], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[] | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Float, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/apps/order-service/src/util/IntFilter.ts b/apps/order-service/src/util/IntFilter.ts new file mode 100644 index 0000000..f6880e7 --- /dev/null +++ b/apps/order-service/src/util/IntFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Int } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class IntFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + equals?: number; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + in?: number[]; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[]; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/apps/order-service/src/util/IntNullableFilter.ts b/apps/order-service/src/util/IntNullableFilter.ts new file mode 100644 index 0000000..e3b71a3 --- /dev/null +++ b/apps/order-service/src/util/IntNullableFilter.ts @@ -0,0 +1,98 @@ +import { Field, InputType, Int } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class IntNullableFilter { + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + equals?: number | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + in?: number[] | null; + + @ApiProperty({ + required: false, + type: [Number], + }) + @IsOptional() + @Field(() => [Int], { + nullable: true, + }) + @Type(() => Number) + notIn?: number[] | null; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + lte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gt?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + gte?: number; + + @ApiProperty({ + required: false, + type: Number, + }) + @IsOptional() + @Field(() => Int, { + nullable: true, + }) + @Type(() => Number) + not?: number; +} diff --git a/apps/order-service/src/util/JsonFilter.ts b/apps/order-service/src/util/JsonFilter.ts new file mode 100644 index 0000000..7040b74 --- /dev/null +++ b/apps/order-service/src/util/JsonFilter.ts @@ -0,0 +1,31 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { GraphQLJSONObject } from "graphql-type-json"; +import { InputJsonValue } from "../types"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class JsonFilter { + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + equals?: InputJsonValue; + + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + not?: InputJsonValue; +} diff --git a/apps/order-service/src/util/JsonNullableFilter.ts b/apps/order-service/src/util/JsonNullableFilter.ts new file mode 100644 index 0000000..3381d52 --- /dev/null +++ b/apps/order-service/src/util/JsonNullableFilter.ts @@ -0,0 +1,31 @@ +import type { JsonValue } from "type-fest"; +import { Field, InputType } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { GraphQLJSONObject } from "graphql-type-json"; + +@InputType({ + isAbstract: true, + description: undefined, +}) +export class JsonNullableFilter { + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + equals?: JsonValue; + + @ApiProperty({ + required: false, + type: GraphQLJSONObject, + }) + @IsOptional() + @Field(() => GraphQLJSONObject, { + nullable: true, + }) + not?: JsonValue; +} diff --git a/apps/order-service/src/util/MetaQueryPayload.ts b/apps/order-service/src/util/MetaQueryPayload.ts new file mode 100644 index 0000000..fc30531 --- /dev/null +++ b/apps/order-service/src/util/MetaQueryPayload.ts @@ -0,0 +1,13 @@ +import { ObjectType, Field } from "@nestjs/graphql"; +import { ApiProperty } from "@nestjs/swagger"; + +@ObjectType() +class MetaQueryPayload { + @ApiProperty({ + required: true, + type: [Number], + }) + @Field(() => Number) + count!: number; +} +export { MetaQueryPayload }; diff --git a/apps/order-service/src/util/QueryMode.ts b/apps/order-service/src/util/QueryMode.ts new file mode 100644 index 0000000..f9b1653 --- /dev/null +++ b/apps/order-service/src/util/QueryMode.ts @@ -0,0 +1,10 @@ +import { registerEnumType } from "@nestjs/graphql"; + +export enum QueryMode { + Default = "default", + Insensitive = "insensitive", +} +registerEnumType(QueryMode, { + name: "QueryMode", + description: undefined, +}); diff --git a/apps/order-service/src/util/SortOrder.ts b/apps/order-service/src/util/SortOrder.ts new file mode 100644 index 0000000..d4108e6 --- /dev/null +++ b/apps/order-service/src/util/SortOrder.ts @@ -0,0 +1,10 @@ +import { registerEnumType } from "@nestjs/graphql"; + +export enum SortOrder { + Asc = "asc", + Desc = "desc", +} +registerEnumType(SortOrder, { + name: "SortOrder", + description: undefined, +}); diff --git a/apps/order-service/src/util/StringFilter.ts b/apps/order-service/src/util/StringFilter.ts new file mode 100644 index 0000000..80b573d --- /dev/null +++ b/apps/order-service/src/util/StringFilter.ts @@ -0,0 +1,141 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { QueryMode } from "./QueryMode"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, +}) +export class StringFilter { + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + equals?: string; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + in?: string[]; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + notIn?: string[]; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + contains?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + startsWith?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + endsWith?: string; + + @ApiProperty({ + required: false, + enum: ["Default", "Insensitive"], + }) + @IsOptional() + @Field(() => QueryMode, { + nullable: true, + }) + mode?: QueryMode; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + not?: string; +} diff --git a/apps/order-service/src/util/StringNullableFilter.ts b/apps/order-service/src/util/StringNullableFilter.ts new file mode 100644 index 0000000..01b399c --- /dev/null +++ b/apps/order-service/src/util/StringNullableFilter.ts @@ -0,0 +1,141 @@ +import { Field, InputType } from "@nestjs/graphql"; +import { QueryMode } from "./QueryMode"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +@InputType({ + isAbstract: true, +}) +export class StringNullableFilter { + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + equals?: string | null; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + in?: string[] | null; + + @ApiProperty({ + required: false, + type: [String], + }) + @IsOptional() + @Field(() => [String], { + nullable: true, + }) + @Type(() => String) + notIn?: string[] | null; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + lte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gt?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + gte?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + contains?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + startsWith?: string; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + endsWith?: string; + + @ApiProperty({ + required: false, + enum: ["Default", "Insensitive"], + }) + @IsOptional() + @Field(() => QueryMode, { + nullable: true, + }) + mode?: QueryMode; + + @ApiProperty({ + required: false, + type: String, + }) + @IsOptional() + @Field(() => String, { + nullable: true, + }) + @Type(() => String) + not?: string; +} diff --git a/apps/order-service/src/validators/index.ts b/apps/order-service/src/validators/index.ts new file mode 100644 index 0000000..7f62d84 --- /dev/null +++ b/apps/order-service/src/validators/index.ts @@ -0,0 +1 @@ +export * from "./is-json-value-validator"; diff --git a/apps/order-service/src/validators/is-json-value-validator.spec.ts b/apps/order-service/src/validators/is-json-value-validator.spec.ts new file mode 100644 index 0000000..5a77824 --- /dev/null +++ b/apps/order-service/src/validators/is-json-value-validator.spec.ts @@ -0,0 +1,44 @@ +import { validate, ValidationError } from "class-validator"; +import { IsJSONValue } from "./is-json-value-validator"; + +class TestClass { + @IsJSONValue() + jsonProperty: unknown; +} + +describe("IsJSONValue", () => { + it("should validate a valid JSON string", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = '{"name": "John", "age": 30}'; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(0); + }); + + it("should not validate an invalid JSON string", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = '{name: "John", age: 30}'; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(1); + }); + + it("should not validate an invalid JSON string", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = "John"; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(1); + }); + + it("should validate a valid JSON object", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = { name: "John", age: 30 }; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(0); + }); + + it("should validate a valid JSON array", async () => { + const testObj = new TestClass(); + testObj.jsonProperty = ["John", "30"]; + const errors: ValidationError[] = await validate(testObj); + expect(errors.length).toBe(0); + }); +}); diff --git a/apps/order-service/src/validators/is-json-value-validator.ts b/apps/order-service/src/validators/is-json-value-validator.ts new file mode 100644 index 0000000..1002540 --- /dev/null +++ b/apps/order-service/src/validators/is-json-value-validator.ts @@ -0,0 +1,29 @@ +import { + ValidationArguments, + registerDecorator, + ValidationOptions, +} from "class-validator"; +import isJSONValidator from "validator/lib/isJSON"; + +export function IsJSONValue(validationOptions?: ValidationOptions) { + return function (object: Record<string, any>, propertyName: string) { + registerDecorator({ + name: "IsJSONValue", + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + if (typeof value === "string") { + return isJSONValidator(value); + } + + return isJSONValidator(JSON.stringify(value)); + }, + defaultMessage(args: ValidationArguments): string { + return `${args.property} must be a valid json`; + }, + }, + }); + }; +} diff --git a/apps/order-service/tsconfig.build.json b/apps/order-service/tsconfig.build.json new file mode 100644 index 0000000..e579401 --- /dev/null +++ b/apps/order-service/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "prisma", "test", "dist", "**/*spec.ts", "admin"] +} diff --git a/apps/order-service/tsconfig.json b/apps/order-service/tsconfig.json new file mode 100644 index 0000000..707e8cd --- /dev/null +++ b/apps/order-service/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "module": "commonjs", + "declaration": false, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es2022", + "lib": ["es2023"], + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["src"] +}