diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml new file mode 100644 index 00000000..25fa5cc5 --- /dev/null +++ b/.github/workflows/cli.yml @@ -0,0 +1,32 @@ +name: CLI tests +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Check out frontend + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install dependencies + run: npm ci + - name: Generate resources and fields + run: npm run generate:resource -- --name=Department + - run: npm run generate:resource -- --name=People + - run: npm run generate:field -- --name=Department --property=name --kind=primitive --type=string --isOptional=false --isShowInTable=true + - run: npm run generate:field -- --name=People --property=profilePicture --kind=reference --referenceType=toOne --type=File --isOptional=true --isShowInTable=true + - run: npm run generate:field -- --name=People --property=firstName --kind=primitive --type=string --isOptional=false --isShowInTable=true + - run: npm run generate:field -- --name=People --property=lastName --kind=primitive --type=string --isOptional=false --isShowInTable=true + - run: npm run generate:field -- --name=People --property=department --kind=reference --referenceType=toOne --type=Department --isOptional=true --isShowInTable=true --propertyForSelect=name + - run: npm run generate:field -- --name=People --property=isActive --kind=primitive --type=boolean --isOptional=false --isShowInTable=true + - run: npm run generate:field -- --name=People --property=birthDate --kind=primitive --type=Date --isOptional=true --isShowInTable=true + - run: npm run generate:field -- --name=People --property=hireDate --kind=primitive --type=Date --isOptional=false --isShowInTable=true + - run: npm run generate:field -- --name=People --property=salary --kind=primitive --type=number --isOptional=false --isShowInTable=true + - name: Build + run: npm run build diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e5bdb2dc..da27729a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,51 +1,52 @@ name: E2E tests on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - name: Check out backend - uses: actions/checkout@v4 - with: - repository: brocoders/nestjs-boilerplate - # Use token for private repository - # token: ${{ secrets.CI_PAT }} - path: backend - - run: cd backend && cp env-example-document .env - - run: cd backend && sed -i 's/APP_PORT=3000/APP_PORT=3001/g' .env - - run: cd backend && sed -i 's/BACKEND_DOMAIN=http:\/\/localhost:3000/BACKEND_DOMAIN=http:\/\/localhost:3001/g' .env - - name: Run backend - # print output of the command to file and store it as artifact - run: cd backend && docker compose -f docker-compose.document.yaml up > ${{ runner.temp }}/backend.log 2>&1 & - - run: cd backend && sed -i 's/\r//g' wait-for-it.sh - - run: cd backend && ./wait-for-it.sh localhost:3001 -- echo "Backend is up" + - name: Check out backend + uses: actions/checkout@v4 + with: + repository: brocoders/nestjs-boilerplate + # Use token for private repository + # token: ${{ secrets.CI_PAT }} + path: backend + - run: cd backend && cp env-example-document .env + - run: cd backend && sed -i 's/APP_PORT=3000/APP_PORT=3001/g' .env + - run: cd backend && sed -i 's/BACKEND_DOMAIN=http:\/\/localhost:3000/BACKEND_DOMAIN=http:\/\/localhost:3001/g' .env + - name: Run backend + # print output of the command to file and store it as artifact + run: cd backend && docker compose -f docker-compose.document.yaml up > ${{ runner.temp }}/backend.log 2>&1 & + - run: cd backend && sed -i 's/\r//g' wait-for-it.sh + - run: cd backend && ./wait-for-it.sh localhost:3001 -- echo "Backend is up" - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Install dependencies - run: npm ci - - name: Run lint - run: npm run lint - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: backend-log - path: ${{ runner.temp }}/backend.log - retention-days: 30 \ No newline at end of file + - name: Check out frontend + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install dependencies + run: npm ci + - name: Run lint + run: npm run lint + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: backend-log + path: ${{ runner.temp }}/backend.log + retention-days: 30 diff --git a/.hygen/generate/field/create/field-component-reference.ejs.t b/.hygen/generate/field/create/field-component-reference.ejs.t index 70ee213e..763163b8 100644 --- a/.hygen/generate/field/create/field-component-reference.ejs.t +++ b/.hygen/generate/field/create/field-component-reference.ejs.t @@ -9,7 +9,7 @@ before: \ function <%= h.inflection.camelize(property, false) %>Field() { const { t } = useTranslation("admin-panel-<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>-create"); const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useGet<%= h.inflection.transform(type, ['pluralize']) %>Query(); + useGet<%= h.inflection.transform(type, ['pluralize']) %>ListQuery(); const options = useMemo( () => (data?.pages.flatMap((page) => page?.data ?? []).filter(Boolean) ?? []), diff --git a/.hygen/generate/field/create/field-property-type.ejs.t b/.hygen/generate/field/create/field-property-type.ejs.t index e9f0d49a..d39c38fb 100644 --- a/.hygen/generate/field/create/field-property-type.ejs.t +++ b/.hygen/generate/field/create/field-property-type.ejs.t @@ -6,13 +6,13 @@ after: type CreateFormData <% if (kind === 'primitive') { -%> <% if (type === 'string') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: string; + <%= property %>: string; <% } else if (type === 'number') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: string; + <%= property %>: string; <% } else if (type === 'boolean') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: boolean; + <%= property %>: boolean; <% } else if (type === 'Date') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: Date | null; + <%= property %>: Date | null; <% } -%> <% } else if (kind === 'reference') { -%> <% if (referenceType === 'toMany') { -%> @@ -23,9 +23,9 @@ after: type CreateFormData <% } -%> <% } else { -%> <% if (type === 'File') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: FileEntity | null; + <%= property %>: FileEntity | null; <% } else { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: <%= type %> | null; + <%= property %>: <%= type %> | null; <% } -%> <% } -%> <% } -%> diff --git a/.hygen/generate/field/create/field-property-validation.ejs.t b/.hygen/generate/field/create/field-property-validation.ejs.t index 8bd2e8a1..7b87177a 100644 --- a/.hygen/generate/field/create/field-property-validation.ejs.t +++ b/.hygen/generate/field/create/field-property-validation.ejs.t @@ -10,6 +10,8 @@ before: \ .string() <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) + <% } else { -%> + .defined() <% } -%> , <% } else if (type === 'number') { -%> @@ -17,17 +19,22 @@ before: \ .string() <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) + <% } else { -%> + .defined() <% } -%> , <% } else if (type === 'boolean') { -%> - <%= property %>: yup.boolean(), + <%= property %>: yup.boolean().defined(), <% } else if (type === 'Date') { -%> <%= property %>: yup .date() <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) <% } -%> - .nullable(), + .nullable() + <% if (isOptional) { -%> + .defined() + <% } -%>, <% } -%> <% } else if (kind === 'reference') { -%> <% if (referenceType === 'toMany') { -%> @@ -57,6 +64,9 @@ before: \ <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) <% } -%> - .nullable(), + .nullable() + <% if (isOptional) { -%> + .defined() + <% } -%>, <% } -%> <% } -%> diff --git a/.hygen/generate/field/create/import-type-query.ejs.t b/.hygen/generate/field/create/import-type-query.ejs.t index 1185a8b0..1575090b 100644 --- a/.hygen/generate/field/create/import-type-query.ejs.t +++ b/.hygen/generate/field/create/import-type-query.ejs.t @@ -2,11 +2,11 @@ inject: true to: src/app/[language]/admin-panel/<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>/create/page-content.tsx at_line: 2 -skip_if: import { useGet<%= h.inflection.transform(type, ['pluralize']) %>Query } +skip_if: import { useGet<%= h.inflection.transform(type, ['pluralize']) %>ListQuery } --- <% if (kind === 'reference') { -%> <% if (type !== 'File') { -%> - import { useGet<%= h.inflection.transform(type, ['pluralize']) %>Query } from "@/app/[language]/admin-panel/<%= h.inflection.transform(type, ['pluralize', 'underscore', 'dasherize']) %>/queries/queries"; + import { useGet<%= h.inflection.transform(type, ['pluralize']) %>ListQuery } from "@/app/[language]/admin-panel/<%= h.inflection.transform(type, ['pluralize', 'underscore', 'dasherize']) %>/queries/queries"; <% } -%> <% } -%> diff --git a/.hygen/generate/field/edit/field-component-reference.ejs.t b/.hygen/generate/field/edit/field-component-reference.ejs.t index 6fca72f2..00b9898b 100644 --- a/.hygen/generate/field/edit/field-component-reference.ejs.t +++ b/.hygen/generate/field/edit/field-component-reference.ejs.t @@ -9,7 +9,7 @@ before: \ function <%= h.inflection.camelize(property, false) %>Field() { const { t } = useTranslation("admin-panel-<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>-edit"); const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useGet<%= h.inflection.transform(type, ['pluralize']) %>Query(); + useGet<%= h.inflection.transform(type, ['pluralize']) %>ListQuery(); const options = useMemo( () => (data?.pages.flatMap((page) => page?.data ?? []).filter(Boolean) ?? []), diff --git a/.hygen/generate/field/edit/field-property-type.ejs.t b/.hygen/generate/field/edit/field-property-type.ejs.t index 9cf7c531..1fac443d 100644 --- a/.hygen/generate/field/edit/field-property-type.ejs.t +++ b/.hygen/generate/field/edit/field-property-type.ejs.t @@ -6,13 +6,13 @@ after: type EditFormData <% if (kind === 'primitive') { -%> <% if (type === 'string') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: string; + <%= property %>: string; <% } else if (type === 'number') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: string; + <%= property %>: string; <% } else if (type === 'boolean') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: boolean; + <%= property %>: boolean; <% } else if (type === 'Date') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: Date | null; + <%= property %>: Date | null; <% } -%> <% } else if (kind === 'reference') { -%> <% if (referenceType === 'toMany') { -%> @@ -23,9 +23,9 @@ after: type EditFormData <% } -%> <% } else { -%> <% if (type === 'File') { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: FileEntity | null; + <%= property %>: FileEntity | null; <% } else { -%> - <%= property %><% if (isOptional) { -%>?<% } -%>: <%= type %> | null; + <%= property %>: <%= type %> | null; <% } -%> <% } -%> <% } -%> diff --git a/.hygen/generate/field/edit/field-property-validation.ejs.t b/.hygen/generate/field/edit/field-property-validation.ejs.t index 8a7a3fac..c91f9a56 100644 --- a/.hygen/generate/field/edit/field-property-validation.ejs.t +++ b/.hygen/generate/field/edit/field-property-validation.ejs.t @@ -10,6 +10,8 @@ before: \ .string() <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) + <% } else { -%> + .defined() <% } -%> , <% } else if (type === 'number') { -%> @@ -17,17 +19,22 @@ before: \ .string() <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) + <% } else { -%> + .defined() <% } -%> , <% } else if (type === 'boolean') { -%> - <%= property %>: yup.boolean(), + <%= property %>: yup.boolean().defined(), <% } else if (type === 'Date') { -%> <%= property %>: yup .date() <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) <% } -%> - .nullable(), + .nullable() + <% if (isOptional) { -%> + .defined() + <% } -%>, <% } -%> <% } else if (kind === 'reference') { -%> <% if (referenceType === 'toMany') { -%> @@ -57,6 +64,9 @@ before: \ <% if (!isOptional) { -%> .required(t("inputs.<%= property %>.validation.required")) <% } -%> - .nullable(), + .nullable() + <% if (isOptional) { -%> + .defined() + <% } -%>, <% } -%> <% } -%> diff --git a/.hygen/generate/field/edit/import-type-query.ejs.t b/.hygen/generate/field/edit/import-type-query.ejs.t index a1351330..a86adaee 100644 --- a/.hygen/generate/field/edit/import-type-query.ejs.t +++ b/.hygen/generate/field/edit/import-type-query.ejs.t @@ -2,11 +2,11 @@ inject: true to: src/app/[language]/admin-panel/<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>/edit/[id]/page-content.tsx at_line: 2 -skip_if: import { useGet<%= h.inflection.transform(type, ['pluralize']) %>Query } +skip_if: import { useGet<%= h.inflection.transform(type, ['pluralize']) %>ListQuery } --- <% if (kind === 'reference') { -%> <% if (type !== 'File') { -%> - import { useGet<%= h.inflection.transform(type, ['pluralize']) %>Query } from "@/app/[language]/admin-panel/<%= h.inflection.transform(type, ['pluralize', 'underscore', 'dasherize']) %>/queries/queries"; + import { useGet<%= h.inflection.transform(type, ['pluralize']) %>ListQuery } from "@/app/[language]/admin-panel/<%= h.inflection.transform(type, ['pluralize', 'underscore', 'dasherize']) %>/queries/queries"; <% } -%> <% } -%> diff --git a/.hygen/generate/resource/api-service.ejs.t b/.hygen/generate/resource/api-service.ejs.t index 7ceb2dc1..e1c7c48b 100644 --- a/.hygen/generate/resource/api-service.ejs.t +++ b/.hygen/generate/resource/api-service.ejs.t @@ -9,18 +9,18 @@ import { InfinityPaginationType } from "../types/infinity-pagination"; import { RequestConfigType } from "./types/request-config"; import { <%= name %> as Entity } from "../types/<%= h.inflection.transform(name, ['underscore', 'dasherize']) %>"; -export type Get<%= h.inflection.transform(name, ['pluralize']) %>Request = { +export type Get<%= h.inflection.transform(name, ['pluralize']) %>ListRequest = { page: number; limit: number; }; -export type Get<%= h.inflection.transform(name, ['pluralize']) %>Response = InfinityPaginationType; +export type Get<%= h.inflection.transform(name, ['pluralize']) %>ListResponse = InfinityPaginationType; -export function useGet<%= h.inflection.transform(name, ['pluralize']) %>Service() { +export function useGet<%= h.inflection.transform(name, ['pluralize']) %>ListService() { const fetch = useFetch(); return useCallback( - (data: Get<%= h.inflection.transform(name, ['pluralize']) %>Request, requestConfig?: RequestConfigType) => { + (data: Get<%= h.inflection.transform(name, ['pluralize']) %>ListRequest, requestConfig?: RequestConfigType) => { const requestUrl = new URL(`${API_URL}/v1/<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>`); requestUrl.searchParams.append("page", data.page.toString()); requestUrl.searchParams.append("limit", data.limit.toString()); @@ -28,7 +28,7 @@ export function useGet<%= h.inflection.transform(name, ['pluralize']) %>Service( return fetch(requestUrl, { method: "GET", ...requestConfig, - }).then(wrapperFetchJsonResponseResponse>); + }).then(wrapperFetchJsonResponseListResponse>); }, [fetch] ); diff --git a/.hygen/generate/resource/page-content.ejs.t b/.hygen/generate/resource/page-content.ejs.t index 2fe23a6e..2001dfe6 100644 --- a/.hygen/generate/resource/page-content.ejs.t +++ b/.hygen/generate/resource/page-content.ejs.t @@ -10,7 +10,7 @@ import Container from "@mui/material/Container"; import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; import { useCallback, useMemo, useRef, useState } from "react"; -import { useGet<%= h.inflection.transform(name, ['pluralize']) %>Query, <%= h.inflection.camelize(h.inflection.pluralize(name), true) %>QueryKeys } from "./queries/queries"; +import { useGet<%= h.inflection.transform(name, ['pluralize']) %>ListQuery, <%= h.inflection.camelize(h.inflection.pluralize(name), true) %>QueryKeys } from "./queries/queries"; import { TableVirtuoso } from "react-virtuoso"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; @@ -172,11 +172,11 @@ function Actions({ entityItem }: { entityItem: <%= name %> }) { ); } -function <%= h.inflection.transform(name, ['pluralize']) %>() { +function <%= h.inflection.transform(name, ['pluralize']) %>PageContent() { const { t } = useTranslation("admin-panel-<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>"); const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useGet<%= h.inflection.transform(name, ['pluralize']) %>Query(); + useGet<%= h.inflection.transform(name, ['pluralize']) %>ListQuery(); const handleScroll = useCallback(() => { if (!hasNextPage || isFetchingNextPage) return; @@ -255,4 +255,4 @@ function <%= h.inflection.transform(name, ['pluralize']) %>() { ); } -export default withPageRequiredAuth(<%= h.inflection.transform(name, ['pluralize']) %>, { roles: [RoleEnum.ADMIN] }); +export default withPageRequiredAuth(<%= h.inflection.transform(name, ['pluralize']) %>PageContent, { roles: [RoleEnum.ADMIN] }); diff --git a/.hygen/generate/resource/page.ejs.t b/.hygen/generate/resource/page.ejs.t index 4b13af17..a3b1ccfe 100644 --- a/.hygen/generate/resource/page.ejs.t +++ b/.hygen/generate/resource/page.ejs.t @@ -1,9 +1,11 @@ --- to: src/app/[language]/admin-panel/<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>/page.tsx --- +"use server"; + import type { Metadata } from "next"; import { getServerTranslation } from "@/services/i18n"; -import <%= h.inflection.transform(name, ['pluralize']) %> from "./page-content"; +import <%= h.inflection.transform(name, ['pluralize']) %>PageContent from "./page-content"; type Props = { params: Promise<{ language: string }>; @@ -22,6 +24,6 @@ export async function generateMetadata(props: Props): Promise { }; } -export default function Page() { - return <<%= h.inflection.transform(name, ['pluralize']) %> />; +export default async function Page() { + return <<%= h.inflection.transform(name, ['pluralize']) %>PageContent />; } diff --git a/.hygen/generate/resource/queries/queries.ejs.t b/.hygen/generate/resource/queries/queries.ejs.t index d317e6c8..2682a85b 100644 --- a/.hygen/generate/resource/queries/queries.ejs.t +++ b/.hygen/generate/resource/queries/queries.ejs.t @@ -9,7 +9,7 @@ import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { Get<%= name %>Request, useGet<%= name %>Service, - useGet<%= h.inflection.transform(name, ['pluralize']) %>Service, + useGet<%= h.inflection.transform(name, ['pluralize']) %>ListService, } from "@/services/api/services/<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>"; export const <%= h.inflection.camelize(h.inflection.pluralize(name), true) %>QueryKeys = createQueryKeys(["<%= h.inflection.transform(name, ['pluralize', 'underscore', 'dasherize']) %>"], { @@ -21,8 +21,8 @@ export const <%= h.inflection.camelize(h.inflection.pluralize(name), true) %>Que }), }); -export const useGet<%= h.inflection.transform(name, ['pluralize']) %>Query = () => { - const fetch = useGet<%= h.inflection.transform(name, ['pluralize']) %>Service(); +export const useGet<%= h.inflection.transform(name, ['pluralize']) %>ListQuery = () => { + const fetch = useGet<%= h.inflection.transform(name, ['pluralize']) %>ListService(); const query = useInfiniteQuery({ queryKey: <%= h.inflection.camelize(h.inflection.pluralize(name), true) %>QueryKeys.list().key,