├── prettierrc.json ├── src ├── templateRoot │ ├── app │ │ ├── globals.css │ │ ├── icon.png │ │ ├── favicon.ico │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── not-found.tsx │ │ ├── rootGroupRouteHome.tsx │ │ ├── groupRouteHome.tsx │ │ ├── nexquikTemplateModel │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ ├── [id] │ │ │ │ ├── edit │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ └── rootGroupRouteLayout.tsx │ ├── public │ │ ├── logo.png │ │ └── backdrop.png │ ├── postcss.config.js │ ├── next.config.js │ ├── tailwind.config.js │ ├── lib │ │ └── prisma.ts │ ├── .env │ ├── prisma │ │ └── schema.prisma │ ├── README.md │ ├── .gitignore │ ├── tsconfig.json │ └── package.json ├── index.ts ├── prismaGenerator.ts ├── modelTree.ts ├── cli.ts ├── helpers.ts └── generators.ts ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── babel.config.js ├── .gitignore ├── example ├── package.json └── schema.prisma ├── .npmignore ├── docker-compose.yml ├── renovate.json ├── .codesandbox ├── template.json └── tasks.json ├── __tests__ ├── prisma-schemas │ ├── minimal-examples │ │ ├── many-to-many.prisma │ │ ├── enums.prisma │ │ ├── one-to-many.prisma │ │ ├── composite-id-relation.prisma │ │ └── composite-types.prisma │ ├── not-working │ │ └── multiple-related-fields.prisma │ └── schemas.test.ts ├── core-functionality-tests │ ├── schema.prisma │ ├── prisma-generator.test.ts │ └── cli-arg.test.ts └── utils.ts ├── tsconfig.json ├── .github ├── dependabot.yml └── workflows │ └── publish.yml ├── prisma └── dev.prisma ├── release.config.js ├── .gitattributes ├── CONTRIBUTING.md ├── template.eslintrc.json ├── package.json ├── jest.config.js ├── README.md ├── LICENSE └── CHANGELOG.md /prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/templateRoot/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-bullseye 2 | 3 | # The workspace will be mounted under /workspace -------------------------------------------------------------------------------- /src/templateRoot/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/app/icon.png -------------------------------------------------------------------------------- /src/templateRoot/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/app/favicon.ico -------------------------------------------------------------------------------- /src/templateRoot/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/public/logo.png -------------------------------------------------------------------------------- /src/templateRoot/public/backdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcanfield/nexquik/HEAD/src/templateRoot/public/backdrop.png -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Devcontainer", 3 | "build": { 4 | "dockerfile": "./Dockerfile" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/templateRoot/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import chalk from "chalk"; 3 | import { run } from "./cli"; 4 | console.log(chalk.gray("Running Nexquik")); 5 | run(); 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { targets: { node: "current" } }], 4 | "@babel/preset-typescript", 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /src/templateRoot/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | serverActions: true, 6 | }, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | *.tsbuildinfo 4 | 5 | # Development 6 | devNexquikApp/ 7 | nexquik.tgz 8 | 9 | # Testing 10 | testOutputDirectory 11 | 12 | # Allows for local testing using a private schema 13 | private-schema.prisma -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nexquik-live-demo", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "init": "nexquik --init group --name UserManagement --include Organization" 6 | }, 7 | "dependencies": { 8 | "nexquik": "*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/**/* 2 | node_modules/ 3 | prisma/ 4 | prisma_large/ 5 | prisma_non_working/ 6 | devNexquikApp/ 7 | templateRoot/ 8 | .github/ 9 | __tests__/ 10 | .gitattributes 11 | babel.config.js 12 | jest.config.js 13 | prettierrc.json 14 | release.config.js 15 | schema.prisma 16 | example/ -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | container_name: nexquik-db 6 | image: postgres:11 7 | restart: always 8 | ports: 9 | - 5444:5432 10 | environment: 11 | POSTGRES_PASSWORD: password 12 | POSTGRES_USER: nexquik 13 | POSTGRES_DB: nexquik-db 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch"], 9 | "matchCurrentVersion": "!/^0/", 10 | "automerge": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/templateRoot/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Roboto } from "next/font/google"; 2 | 3 | // const roboto = Roboto({ 4 | // weight: "400", 5 | // subsets: ["latin"], 6 | // display: "swap", 7 | // }); 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | return <>{children}; 14 | } 15 | -------------------------------------------------------------------------------- /src/templateRoot/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default async function Home() { 4 | // @nexquik homeRedirect start 5 | redirect("/gen"); 6 | // @nexquik homeRedirect stop 7 | return ( 8 |
9 | APP DIR Home Page 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/templateRoot/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./app/**/*.{js,ts,jsx,tsx,mdx}"], 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: ["Roboto", "ui-sans-serif", "system-ui"], 8 | }, 9 | maxWidth: { 10 | "8xl": "90rem", 11 | }, 12 | }, 13 | }, 14 | plugins: [require("@tailwindcss/forms")], 15 | }; 16 | -------------------------------------------------------------------------------- /.codesandbox/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Nexquik Live Demo", 3 | "description": "This is an example of Nexquik generating a full app from only a Prisma Schema. Then it will re-generate your components in your app directory as you change your Schema.", 4 | "tags": [ 5 | "nextjs", 6 | "prisma", 7 | "typescript", 8 | "react", 9 | "app-router", 10 | "tailwindcss", 11 | "nexquik" 12 | ], 13 | "published": true 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/prisma-schemas/minimal-examples/many-to-many.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | id Int @id @default(autoincrement()) 12 | name String 13 | tags Tag[] 14 | } 15 | 16 | model Tag { 17 | id Int @id @default(autoincrement()) 18 | name String 19 | users User[] 20 | } 21 | -------------------------------------------------------------------------------- /src/templateRoot/lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | let prisma: PrismaClient; 4 | 5 | if (process.env.NODE_ENV === "production") { 6 | prisma = new PrismaClient(); 7 | } else { 8 | const globalWithPrisma = global as typeof globalThis & { 9 | prisma: PrismaClient; 10 | }; 11 | if (!globalWithPrisma.prisma) { 12 | globalWithPrisma.prisma = new PrismaClient(); 13 | } 14 | prisma = globalWithPrisma.prisma; 15 | } 16 | 17 | export default prisma; 18 | -------------------------------------------------------------------------------- /__tests__/prisma-schemas/minimal-examples/enums.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | enum Status { 11 | TODO 12 | IN_PROGRESS 13 | DONE 14 | } 15 | 16 | model Task { 17 | id Int @id @default(autoincrement()) 18 | title String 19 | status Status 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @updatedAt 22 | } 23 | -------------------------------------------------------------------------------- /src/prismaGenerator.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { generatorHandler } from "@prisma/generator-helper"; 3 | import { defaultOutputDirectory, run } from "./cli"; 4 | import chalk from "chalk"; 5 | 6 | console.log(chalk.gray("Running Nexquik as Prisma Generator")); 7 | generatorHandler({ 8 | onManifest() { 9 | return { 10 | prettyName: "Nexquik", 11 | version: require("../package.json").version, 12 | defaultOutput: defaultOutputDirectory, 13 | }; 14 | }, 15 | onGenerate: run, 16 | }); 17 | -------------------------------------------------------------------------------- /src/templateRoot/.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL=postgresql://nexquik:password@0.0.0.0:5444/nexquik-db?schema=public 8 | -------------------------------------------------------------------------------- /__tests__/prisma-schemas/minimal-examples/one-to-many.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | id Int @id @default(autoincrement()) 12 | name String 13 | posts Post[] 14 | } 15 | 16 | model Post { 17 | id Int @id @default(autoincrement()) 18 | title String 19 | content String 20 | userId Int 21 | user User @relation(fields: [userId], references: [id]) 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "dist", 5 | "strict": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "sourceMap": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "types": ["node"], 12 | "plugins": [ 13 | { 14 | "name": "next" 15 | } 16 | ] 17 | }, 18 | "exclude": [ 19 | "dist/*", 20 | "src/templateRoot/*", 21 | "devNexquikApp/*", 22 | "example/*", 23 | "nexquikApp/*", 24 | "__tests__" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/templateRoot/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "sqlite" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model Product { 14 | id String @id 15 | name String 16 | foo Foo @relation(fields: [fooId], references: [id]) 17 | fooId String 18 | } 19 | 20 | model Foo { 21 | id String @id 22 | bar String 23 | Product Product[] 24 | } 25 | -------------------------------------------------------------------------------- /src/templateRoot/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project generated with [`Nexquik`](https://github.com/bcanfield/nexquik). 2 | 3 | ## Getting Started 4 | 5 | Change `DATABASE_URL` in your .env file to point to your desired database. 6 | ```zsh 7 | DATABASE_URL="YOUR DB HERE" 8 | ``` 9 | 10 | Install Dependencies 11 | ```zsh 12 | npm install 13 | ``` 14 | 15 | Run Prisma Generate 16 | ```bash 17 | npm run generate 18 | ``` 19 | 20 | Start the dev server 21 | ```bash 22 | npm run dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/src/templateRoot" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | versioning-strategy: increase-if-necessary 13 | -------------------------------------------------------------------------------- /__tests__/prisma-schemas/not-working/multiple-related-fields.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Child { 11 | id Int @id @default(autoincrement()) 12 | Parent Parent? @relation(fields: [ParentName, ParentType], references: [name, type], onDelete: Cascade) 13 | ParentName String? 14 | ParentType String? 15 | } 16 | 17 | model Parent { 18 | name String @id @default(cuid()) 19 | type String 20 | Children Child[] 21 | 22 | @@unique([name, type]) 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/prisma-schemas/minimal-examples/composite-id-relation.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Post { 11 | postId1 Int 12 | postId2 Int 13 | name String 14 | bookings User @relation(fields: [postId1, postId2], references: [userId1, userId2], onDelete: Cascade) 15 | userId1 Int 16 | userId2 Int 17 | 18 | @@id([postId1, postId2]) 19 | } 20 | 21 | model User { 22 | userId1 Int 23 | userId2 Int 24 | name String 25 | posts Post[] 26 | 27 | @@id([userId1, userId2]) 28 | } 29 | -------------------------------------------------------------------------------- /__tests__/prisma-schemas/minimal-examples/composite-types.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "mongodb" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Product { 11 | id String @id @default(auto()) @map("_id") @db.ObjectId 12 | name String 13 | photos Photo[] 14 | foo Foo @relation(fields: [fooId], references: [id]) 15 | fooId String @db.ObjectId 16 | } 17 | 18 | type Photo { 19 | height Int 20 | width Int 21 | url String 22 | } 23 | 24 | model Foo { 25 | id String @id @default(auto()) @map("_id") @db.ObjectId 26 | bar String 27 | Product Product[] 28 | } 29 | -------------------------------------------------------------------------------- /src/templateRoot/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # prisma sqlite db 38 | dev.db 39 | 40 | # prevent package lock from being tracked because it will change every time we build a new local package 41 | package-lock.json -------------------------------------------------------------------------------- /prisma/dev.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | id Int @id @default(autoincrement()) 12 | username String @unique 13 | email String @unique 14 | posts Post[] 15 | } 16 | 17 | model Post { 18 | id Int @id @default(autoincrement()) 19 | title String 20 | content String 21 | status Status // Enum field 22 | author User @relation(fields: [authorId], references: [id]) 23 | authorId Int 24 | } 25 | 26 | // Define the 'Status' enum 27 | enum Status { 28 | PUBLISHED 29 | DRAFT 30 | ARCHIVED 31 | } 32 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | "main", 4 | { 5 | name: "beta", 6 | prerelease: true, 7 | }, 8 | ], 9 | plugins: [ 10 | "@semantic-release/commit-analyzer", 11 | "@semantic-release/release-notes-generator", 12 | [ 13 | "@semantic-release/changelog", 14 | { 15 | changelogFile: "CHANGELOG.md", 16 | }, 17 | ], 18 | "@semantic-release/npm", 19 | "@semantic-release/github", 20 | [ 21 | "@semantic-release/git", 22 | { 23 | assets: ["CHANGELOG.md", "package.json"], 24 | message: 25 | "chore(release): set `package.json` to ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", 26 | }, 27 | ], 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /__tests__/core-functionality-tests/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "sqlite" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | generator nexquik { 11 | provider = "node ./dist/prismaGenerator.js" 12 | command = "group --name Group1 --include Product,Foo --name Group2 --include Product,Foo --init --output __tests__/core-functionality-tests/testOutputDirectory" 13 | disabled = env("DISABLE_NEXQUIK") 14 | } 15 | 16 | model Product { 17 | id String @id 18 | name String 19 | foo Foo @relation(fields: [fooId], references: [id]) 20 | fooId String 21 | } 22 | 23 | model Foo { 24 | id String @id 25 | bar String 26 | Product Product[] 27 | } 28 | -------------------------------------------------------------------------------- /src/templateRoot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Git attributes 2 | # https://git-scm.com/docs/gitattributes 3 | # https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes 4 | 5 | # Normalize line endings for all files that git determines to be text. 6 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvalueauto 7 | * text=auto 8 | 9 | # Normalize line endings to LF on checkin, and do NOT convert to CRLF when checking-out on Windows. 10 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvaluelf 11 | *.txt text eol=lf 12 | *.html text eol=lf 13 | *.md text eol=lf 14 | *.css text eol=lf 15 | *.scss text eol=lf 16 | *.map text eol=lf 17 | *.js text eol=lf 18 | *.jsx text eol=lf 19 | *.ts text eol=lf 20 | *.tsx text eol=lf 21 | *.json text eol=lf 22 | *.yml text eol=lf 23 | *.yaml text eol=lf 24 | *.xml text eol=lf 25 | *.svg text eol=lf 26 | 27 | *.js linguist-detectable=false -------------------------------------------------------------------------------- /src/templateRoot/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default async function NotFound() { 2 | return ( 3 |
4 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/templateRoot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nexquik-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "reset": "prisma migrate reset", 8 | "push": "prisma db push --accept-data-loss", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint", 12 | "typecheck": "tsc", 13 | "generate": "prisma generate" 14 | }, 15 | "dependencies": { 16 | "@prisma/client": "5.4.2", 17 | "clsx": "^2.0.0", 18 | "next": "^13.5.4", 19 | "prisma": "5.4.2", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "typescript": "5.2.2" 23 | }, 24 | "devDependencies": { 25 | "@tailwindcss/forms": "^0.5.6", 26 | "@types/node": "20.8.5", 27 | "@types/react": "18.2.28", 28 | "@types/react-dom": "18.2.13", 29 | "autoprefixer": "^10.4.16", 30 | "nexquik": "*", 31 | "postcss": "^8.4.31", 32 | "tailwindcss": "^3.3.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to Nexquik 2 | 3 | #### **Developing with Nexquik is Easy!** 4 | 5 | - To build and run locally, run: 6 | ``` 7 | npm run dev 8 | ``` 9 | - This command will watch for file changes under the `src` and `templateApp` directories and spin up a local next.js project that is using the output from Nexquik. 10 | - Check out the `dev` folder for the details of the local next.js project 11 | - To run tests, run: 12 | ``` 13 | npm run test 14 | ``` 15 | 16 | #### **Did you find a bug?** 17 | 18 | - **Submit the bug** on [Github](https://github.com/bcanfield/nexquik/issues/new). 19 | - At the bare minimum - please include your prisma schema or a minimal example. 20 | 21 | #### **Did you write a patch that fixes a bug?** 22 | 23 | - Open a new GitHub pull request with the patch. 24 | 25 | - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 26 | 27 | Thank you! 28 | 29 | Nexquik Team 30 | -------------------------------------------------------------------------------- /__tests__/core-functionality-tests/prisma-generator.test.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import { isDirectoryNotEmpty } from "../utils"; 3 | 4 | const testOutputDirectory = 5 | "__tests__/core-functionality-tests/testOutputDirectory"; 6 | 7 | const prismaMain = "./node_modules/prisma/build/index.js"; 8 | 9 | test("prisma-generator", async () => { 10 | child_process.execSync(`rm -rf ${testOutputDirectory}`); 11 | child_process.execSync( 12 | `prisma generate --schema __tests__/core-functionality-tests/schema.prisma`, 13 | { 14 | stdio: "inherit", 15 | } 16 | ); 17 | expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy(); 18 | try { 19 | // Run npm install 20 | child_process.execSync("npm install --quiet", { 21 | cwd: testOutputDirectory, 22 | }); 23 | } catch (error) { 24 | console.error("TypeScript compilation error:", error.message); 25 | throw error; 26 | } finally { 27 | child_process.execSync(`rm -rf ${testOutputDirectory}`); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/templateRoot/app/rootGroupRouteHome.tsx: -------------------------------------------------------------------------------- 1 | export default async function Home() { 2 | return ( 3 |
4 | 18 |
19 | {/* @nexquik routeGroupList start */} 20 | Route 21 | {/* @nexquik routeGroupList stop */} 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/templateRoot/app/groupRouteHome.tsx: -------------------------------------------------------------------------------- 1 | export default async function Home() { 2 | return ( 3 |
4 | 19 |
20 | {/* @nexquik routeList start */} 21 | Route 22 | {/* @nexquik routeList stop */} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /.codesandbox/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // These tasks will run in order when initializing your CodeSandbox project. 3 | "setupTasks": [ 4 | { 5 | "name": "Install Nexquik", 6 | "command": "cd example && npm install" 7 | }, 8 | { 9 | "name": "Generate App with Nexquik", 10 | "command": "cd example && npm run init" 11 | }, 12 | { 13 | "name": "Install Dependencies in new App", 14 | "command": "cd example && npm i" 15 | }, 16 | { 17 | "name": "Set DATABASE_URL environment variable", 18 | "command": "cd example && rm -f .env && echo 'DATABASE_URL=file:./dev.db' > .env" 19 | }, 20 | { 21 | "name": "Push Schema to Database", 22 | "command": "cd example && npx prisma db push" 23 | } 24 | ], 25 | 26 | // These tasks can be run from CodeSandbox. Running one will open a log in the app. 27 | "tasks": { 28 | "dev": { 29 | "name": "dev", 30 | "command": "cd example && npm run dev", 31 | "runAtStart": true, 32 | "preview": { 33 | "port": 3000 34 | } 35 | }, 36 | "generate": { 37 | "name": "generate", 38 | "command": "cd example && npx prisma db push && npm run generate", 39 | "restartOn": { "files": ["**/*.schema"] } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/schema.prisma: -------------------------------------------------------------------------------- 1 | // https://pris.ly/d/prisma-schema 2 | 3 | generator nexquik { 4 | provider = "prisma-generator-nexquik" 5 | command = "group --name UserManagement --include Organization" 6 | } 7 | 8 | generator client { 9 | provider = "prisma-client-js" 10 | } 11 | 12 | datasource db { 13 | provider = "sqlite" 14 | url = env("DATABASE_URL") 15 | } 16 | 17 | model Organization { 18 | id Int @id @default(autoincrement()) 19 | name String 20 | User User[] 21 | } 22 | 23 | model User { 24 | id Int @id @default(autoincrement()) 25 | email String 26 | username String 27 | password String 28 | roles Role[] 29 | organization Organization @relation(fields: [organizationId], references: [id]) 30 | organizationId Int 31 | tasks Task[] 32 | createdAt DateTime @default(now()) 33 | } 34 | 35 | model Role { 36 | id Int @id @default(autoincrement()) 37 | name String 38 | users User[] 39 | } 40 | 41 | model Task { 42 | id Int @id @default(autoincrement()) 43 | name String 44 | description String? 45 | createdAt DateTime @default(now()) 46 | User User? @relation(fields: [userId], references: [id]) 47 | userId Int? 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Build / Publish" 2 | 3 | on: 4 | push: 5 | branches: [main, beta] 6 | pull_request: 7 | branches: [main, beta] 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | packages: write 16 | contents: write 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: 18.x 27 | - name: Install dependencies 28 | run: npx ci 29 | - name: Install semantic-release extra plugins 30 | run: npm install --save-dev @semantic-release/changelog @semantic-release/git 31 | - name: Build 32 | run: npm run build 33 | - name: Test 34 | run: npm run test 35 | - name: Create .npmrc file 36 | run: | 37 | echo registry=https://registry.npmjs.org/ > .npmrc 38 | echo @uzenith360:registry=https://npm.pkg.github.com/ > .npmrc 39 | echo '//npm.pkg.github.com/:_authToken=${{ secrets.GH_TOKEN }}' >> .npmrc 40 | - name: Release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPMAUTHTOKEN }} 44 | run: npx semantic-release 45 | -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | // expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy(); 2 | import * as child_process from "child_process"; 3 | import { readdirSync } from "fs"; 4 | 5 | // export const validateOutputDirectory () 6 | export const isDirectoryNotEmpty = (path: string) => { 7 | try { 8 | const files = readdirSync(path); 9 | return files.length > 0; 10 | } catch (err) { 11 | if (err.code === "ENOENT") { 12 | // Directory doesn't exist 13 | return false; 14 | } else { 15 | // Other error occurred 16 | throw err; 17 | } 18 | } 19 | }; 20 | 21 | export function validateOutputDirectory( 22 | outputDirectory: string, 23 | runGenerate?: boolean 24 | ) { 25 | try { 26 | // Run npm install 27 | child_process.execSync("npm install --quiet", { 28 | cwd: outputDirectory, 29 | }); 30 | // // Run prisma generate 31 | if (runGenerate) { 32 | child_process.execSync(`npm run generate`, { 33 | stdio: "inherit", 34 | cwd: outputDirectory, 35 | }); 36 | } 37 | // Run type check 38 | child_process.execSync("npm run typecheck", { 39 | stdio: "inherit", 40 | cwd: outputDirectory, 41 | }); 42 | } catch (error) { 43 | console.error("TypeScript compilation error:", error.message); 44 | throw error; 45 | } finally { 46 | child_process.execSync(`rm -rf ${outputDirectory}`); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /template.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaFeatures": { 5 | "jsx": true 6 | }, 7 | "ecmaVersion": 2020, 8 | "sourceType": "module" 9 | }, 10 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 11 | "plugins": ["prettier", "@typescript-eslint", "unused-imports"], 12 | "rules": { 13 | "react/react-in-jsx-scope": "off", 14 | // all prettier file errors 15 | // "prettier/prettier": "error", 16 | // "@typescript-eslint/explicit-module-boundary-types": 0, 17 | // "@typescript-eslint/ban-ts-ignore": 0, 18 | // "@typescript-eslint/ban-ts-comment": 0, 19 | // "@typescript-eslint/no-unused-vars": 2, 20 | // "@typescript-eslint/no-explicit-any": 1, 21 | // "no-console": 1, 22 | "unused-imports/no-unused-imports": "error", 23 | "unused-imports/no-unused-vars": [ 24 | "error", 25 | { "vars": "all", "args": "after-used", "ignoreRestSiblings": false } 26 | ], 27 | "import/no-unused-modules": [ 28 | "error", 29 | { 30 | "missingExports": true, 31 | "unusedExports": true 32 | } 33 | ], 34 | "no-unused-vars": [ 35 | "warn", 36 | { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" } 37 | ] 38 | }, 39 | // "ignorePatterns": ["src/server/*.js"], 40 | "globals": { 41 | "React": "writable" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /__tests__/prisma-schemas/schemas.test.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import { readdirSync } from "fs"; 3 | import path from "path"; 4 | import { isDirectoryNotEmpty } from "../utils"; 5 | 6 | const nexquikMain = "./dist/index.js"; 7 | const prismaSchemaDirectory = path.join( 8 | "__tests__", 9 | "prisma-schemas", 10 | "minimal-examples" 11 | ); 12 | const prismaMain = "./node_modules/prisma/build/index.js"; 13 | 14 | // Create an array of schema paths 15 | const schemaPaths = readdirSync(prismaSchemaDirectory); 16 | 17 | // Define a function to run a single schema test asynchronously 18 | async function runSchemaTest(schemaPath) { 19 | const testOutputDirectory = path.join( 20 | "__tests__", 21 | "prisma-schemas", 22 | `${schemaPath}-test-output` 23 | ); 24 | try { 25 | child_process.execSync(`rm -rf ${testOutputDirectory}`); 26 | child_process.execSync( 27 | `node ${nexquikMain} group --name MainGroup --init --rootName gen --schema ${path.join( 28 | prismaSchemaDirectory, 29 | schemaPath 30 | )} --output ${testOutputDirectory}` 31 | ); 32 | 33 | console.log(`Schema Test: ${schemaPath}`); 34 | expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy(); 35 | 36 | // Run npm install 37 | child_process.execSync("npm install --quiet", { 38 | cwd: testOutputDirectory, 39 | }); 40 | // Run next build 41 | child_process.execSync(`npm run build`, { 42 | stdio: "inherit", 43 | cwd: testOutputDirectory, 44 | }); 45 | // Run prisma generate 46 | child_process.execSync(`node ${prismaMain} generate`, { 47 | stdio: "inherit", 48 | cwd: testOutputDirectory, 49 | }); 50 | // Run type check 51 | child_process.execSync("npm run typecheck", { 52 | stdio: "inherit", 53 | cwd: testOutputDirectory, 54 | }); 55 | } catch (error) { 56 | console.error("TypeScript compilation error:", error.message); 57 | throw error; 58 | } finally { 59 | child_process.execSync(`rm -rf ${testOutputDirectory}`); 60 | } 61 | } 62 | 63 | // Run schema tests 64 | schemaPaths.forEach((schemaPath) => { 65 | test(`Schema Test: ${schemaPath}`, async () => { 66 | await runSchemaTest(schemaPath); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/templateRoot/app/nexquikTemplateModel/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { revalidatePath } from "next/cache"; 2 | import Link from "next/link"; 3 | import { redirect } from "next/navigation"; 4 | //@nexquik prismaImport start 5 | import prisma from "@/lib/prisma"; 6 | //@nexquik prismaImport stop 7 | 8 | //@nexquik prismaEnumImport start 9 | import { Enum } from "@prisma/client"; 10 | //@nexquik prismaEnumImport stop 11 | 12 | export default async function CreateNexquikTemplateModel( 13 | //@nexquik props start 14 | { 15 | params, 16 | }: { 17 | params: { [key: string]: string | string[] | undefined }; 18 | } 19 | //@nexquik props stop 20 | ) 21 | 22 | 23 | { 24 | 25 | 26 | async function addNexquikTemplateModel(formData: FormData) { 27 | "use server"; 28 | const created = await prisma.nexquikTemplateModel.create({ 29 | data: 30 | //@nexquik prismaCreateDataInput start 31 | { 32 | name: formData.get("name"), 33 | lat: Number(formData.get("lat")), 34 | lng: Number(formData.get("lng")), 35 | }, 36 | //@nexquik prismaCreateDataInput stop 37 | }); 38 | //@nexquik revalidatePath start 39 | revalidatePath("/nexquikTemplateModel"); 40 | //@nexquik revalidatePath stop 41 | 42 | //@nexquik createRedirect start 43 | redirect(`/nexquikTemplateModel/${created.id}`); 44 | //@nexquik createRedirect stop 45 | } 46 | 47 | return ( 48 |
49 | {/* @nexquik createBreadcrumb start */} 50 | {/* @nexquik createBreadcrumb stop */} 51 | 52 | 59 | {/* @nexquik createForm start */} 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | Cancel 70 | {/* @nexquik createForm stop */} 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/templateRoot/app/nexquikTemplateModel/[id]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { redirect } from "next/navigation"; 3 | //@nexquik prismaImport start 4 | import prisma from "@/lib/prisma"; 5 | //@nexquik prismaImport stop 6 | 7 | //@nexquik prismaEnumImport start 8 | import { Enum } from "@prisma/client"; 9 | //@nexquik prismaEnumImport stop 10 | 11 | export default async function EditNexquikTemplateModel({ 12 | params, 13 | }: { 14 | params: { [key: string]: string | string[] | undefined }; 15 | }) { 16 | const nexquikTemplateModel = await prisma.nexquikTemplateModel.findUnique({ 17 | where: 18 | //@nexquik prismaWhereInput start 19 | { id: params.id }, 20 | //@nexquik prismaWhereInput stop 21 | }); 22 | 23 | async function editNexquikTemplateModel(formData: FormData) { 24 | "use server"; 25 | if (formData) { 26 | await prisma.nexquikTemplateModel.update({ 27 | where: 28 | //@nexquik prismaWhereInput start 29 | { id: params.id }, 30 | //@nexquik prismaWhereInput stop 31 | data: 32 | //@nexquik prismaEditDataInput start 33 | { name: formData.get("name") }, 34 | //@nexquik prismaEditDataInput stop 35 | }); 36 | } 37 | //@nexquik editRedirect start 38 | redirect(`/nexquikTemplateModel/${params.id}`); 39 | //@nexquik editRedirect stop 40 | } 41 | 42 | return ( 43 |
44 | {/* @nexquik editBreadCrumb start */} 45 | {/* @nexquik editBreadCrumb stop */} 46 | {" "} 53 | {/* @nexquik editForm start */} 54 |
55 | 56 | 61 | 62 | 63 | 64 | 69 | 70 |
71 | {/* @nexquik editForm stop */} 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/templateRoot/app/nexquikTemplateModel/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { revalidatePath } from "next/cache"; 2 | import Link from "next/link"; 3 | import { redirect } from "next/navigation"; 4 | //@nexquik prismaImport start 5 | import prisma from "@/lib/prisma"; 6 | //@nexquik prismaImport stop 7 | 8 | export default async function ShowNexquikTemplateModel({ 9 | params, 10 | }: { 11 | params: { [key: string]: string | string[] | undefined }; 12 | }) { 13 | const nexquikTemplateModel = await prisma.nexquikTemplateModel.findUnique({ 14 | where: 15 | //@nexquik prismaWhereInput start 16 | { id: params.id }, 17 | //@nexquik prismaWhereInput stop 18 | }); 19 | async function deleteNexquikTemplateModel(formData: FormData) { 20 | "use server"; 21 | await prisma.nexquikTemplateModel.delete({ 22 | where: 23 | //@nexquik prismaDeleteClause start 24 | { id: formData.get("id") }, 25 | //@nexquik prismaDeleteClause stop 26 | }); 27 | //@nexquik revalidatePath start 28 | revalidatePath("/nexquikTemplateModel"); 29 | //@nexquik revalidatePath stop 30 | 31 | //@nexquik listRedirect start 32 | redirect(`/nexquikTemplateModel`); 33 | //@nexquik listRedirect stop 34 | } 35 | return ( 36 |
37 | {/* @nexquik breadcrumb start */} 38 | {/* @nexquik breadcrumb stop */} 39 | {" "} 46 | {/* @nexquik showForm start */} 47 |
48 | 54 | 55 | Back to All NexquikTemplateModels 56 | 57 | 58 | Edit 59 | 60 | 61 | 72 |
73 | {/* @nexquik showForm stop */} 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /__tests__/core-functionality-tests/cli-arg.test.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import path from "path"; 3 | import { isDirectoryNotEmpty } from "../utils"; 4 | const prismaMain = "./node_modules/prisma/build/index.js"; 5 | 6 | const nexquikMain = "./dist/index.js"; 7 | const schemaPath = path.join( 8 | "__tests__", 9 | "prisma-schemas", 10 | "minimal-examples", 11 | "one-to-many.prisma" 12 | ); 13 | const testOutputDirectory = path.join( 14 | "__tests__", 15 | "core-functionality-tests", 16 | `testOutputDirectory` 17 | ); 18 | 19 | // Create an array of schema paths 20 | const commands: { 21 | command: string; 22 | removePreviousDirectory: boolean; 23 | name: string; 24 | }[] = [ 25 | // Init 26 | { 27 | command: `node ${nexquikMain} group --name MainGroup --init --rootName gen --schema ${schemaPath} --output ${testOutputDirectory}`, 28 | removePreviousDirectory: true, 29 | name: "Init", 30 | }, 31 | // Deps 32 | { 33 | command: `node ${nexquikMain} deps --output ${testOutputDirectory}`, 34 | removePreviousDirectory: false, 35 | name: "Deps", 36 | }, 37 | // Extend Only 38 | { 39 | command: `node ${nexquikMain} group --name MainGroup --extendOnly --rootName gen --schema ${schemaPath} --output ${testOutputDirectory}`, 40 | removePreviousDirectory: false, 41 | name: "Extend Only", 42 | }, 43 | // Prisma Import 44 | { 45 | command: `node ${nexquikMain} group --name MainGroup --rootName gen --schema ${schemaPath} --output ${testOutputDirectory} --prismaImport 'import prisma from "@/lib/prisma";'`, 46 | removePreviousDirectory: false, 47 | name: "Prisma Import", 48 | }, 49 | // Depth 50 | { 51 | command: `node ${nexquikMain} group --name MainGroup --extendOnly --rootName gen --schema ${schemaPath} --output ${testOutputDirectory} --depth 1`, 52 | removePreviousDirectory: false, 53 | name: "Depth", 54 | }, 55 | // Disabled 56 | { 57 | command: `node ${nexquikMain} group --name MainGroup --extendOnly --rootName gen --schema ${schemaPath} --output ${testOutputDirectory} --disabled`, 58 | removePreviousDirectory: false, 59 | name: "Disabled", 60 | }, 61 | ]; 62 | 63 | // Define a function to run a single schema test asynchronously 64 | async function runSchemaTest({ command, removePreviousDirectory }) { 65 | try { 66 | if (removePreviousDirectory) { 67 | child_process.execSync(`rm -rf ${testOutputDirectory}`); 68 | } 69 | child_process.execSync(command); 70 | expect(isDirectoryNotEmpty(testOutputDirectory)).toBeTruthy(); 71 | 72 | // Run npm install 73 | child_process.execSync("npm install --quiet", { 74 | cwd: testOutputDirectory, 75 | }); 76 | // Run next build 77 | child_process.execSync(`npm run build`, { 78 | stdio: "inherit", 79 | cwd: testOutputDirectory, 80 | }); 81 | // Run prisma generate 82 | child_process.execSync(`node ${prismaMain} generate`, { 83 | stdio: "inherit", 84 | cwd: testOutputDirectory, 85 | }); 86 | // Run type check 87 | child_process.execSync("npm run typecheck", { 88 | stdio: "inherit", 89 | cwd: testOutputDirectory, 90 | }); 91 | } catch (error) { 92 | console.error("Command Test error:", error.message); 93 | throw error; 94 | } 95 | } 96 | 97 | // Run schema tests 98 | commands.forEach((command) => { 99 | test(`Schema Test: ${command}`, async () => { 100 | await runSchemaTest(command); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/templateRoot/app/nexquikTemplateModel/page.tsx: -------------------------------------------------------------------------------- 1 | import { revalidatePath } from "next/cache"; 2 | import Link from "next/link"; 3 | //@nexquik prismaImport start 4 | import prisma from "@/lib/prisma"; 5 | //@nexquik prismaImport stop 6 | import clsx from "clsx"; 7 | 8 | export default async function ListNexquikTemplateModels( 9 | //@nexquik listProps start 10 | { 11 | params, 12 | searchParams, 13 | }: { 14 | params: { [key: string]: string | string[] | undefined }; 15 | searchParams?: { [key: string]: string | string[] | undefined }; 16 | } 17 | //@nexquik listProps stop 18 | ) { 19 | /* @nexquik listCount start */ 20 | 21 | /* @nexquik listCount stop */ 22 | 23 | const nexquikTemplateModel = await prisma.nexquikTemplateModel 24 | .findMany 25 | //@nexquik prismaWhereParentClause start 26 | (); 27 | //@nexquik prismaWhereParentClause stop 28 | 29 | async function deleteNexquikTemplateModel(formData: FormData) { 30 | "use server"; 31 | await prisma.nexquikTemplateModel.delete({ 32 | where: 33 | //@nexquik prismaDeleteClause start 34 | { id: formData.get("id") }, 35 | //@nexquik prismaDeleteClause stop 36 | }); 37 | //@nexquik revalidatePath start 38 | revalidatePath("/nexquikTemplateModel"); 39 | //@nexquik revalidatePath stop 40 | } 41 | return ( 42 |
43 | {/* @nexquik listBreadcrumb start */} 44 | {/* @nexquik listBreadcrumb stop */} 45 | 52 |
53 | {/* @nexquik createLink start */} 54 | 55 | Create New NexquikTemplateModel 56 | 57 | {/* @nexquik createLink stop */} 58 |
59 | 60 | {/* @nexquik listForm start */} 61 | 90 | {/* @nexquik listForm stop */} 91 | 92 | {/* @nexquik listPagination start */} 93 | {/* @nexquik listPagination stop */} 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nexquik", 3 | "version": "3.2.1", 4 | "description": "Generate Next.js components from your Prisma Schema", 5 | "main": "dist/index.js", 6 | "repository": "https://github.com/bcanfield/nexquik", 7 | "bin": { 8 | "nexquik": "./dist/index.js", 9 | "prisma-generator-nexquik": "./dist/prismaGenerator.js" 10 | }, 11 | "scripts": { 12 | "build": "npm-run-all build:*", 13 | "build:1": "npm run clean", 14 | "build:2": "npx tsc", 15 | "build:3": "npm run copy-files", 16 | "build:4": "chmod -R 777 dist/", 17 | "install-locally": "npm-run-all install-locally:*", 18 | "install-locally:1": "npm run build", 19 | "install-locally:2": "rm -f ./nexquik.tgz", 20 | "install-locally:3": "npm pack --force", 21 | "install-locally:4": "mv \"$(ls *.tgz)\" \"nexquik.tgz\"", 22 | "install-locally:5": "npm i && npm i --save-dev ./nexquik.tgz", 23 | "dev": "nodemon", 24 | "dev:1": "npm run install-locally", 25 | "dev:2": "rm -rf ./devNexquikApp", 26 | "dev:3": "npm run nexquikInit", 27 | "dev:4": "cd ./devNexquikApp && npm i && npm run generate && npm run push --accept-data-loss && npm run dev", 28 | "nexquikInit": "nexquik group --name Main --init --rootName nexquik --schema ./prisma/dev.prisma --output ./devNexquikApp --appTitle Nexquik", 29 | "nexquikHelp": "nexquik -h", 30 | "test": "jest --silent=false", 31 | "clean": "rimraf dist/", 32 | "generate": "prisma generate", 33 | "copy-files": "npm-run-all copy-files:*", 34 | "copy-files:1": "copyfiles -a -u 1 \"src/templateRoot/**/*\" dist", 35 | "copy-files:2": "copyfiles template.eslintrc.json dist", 36 | "live-example-install": "cd example && npm i" 37 | }, 38 | "nodemonConfig": { 39 | "watch": [ 40 | "src" 41 | ], 42 | "ext": "ts,tsx,css,", 43 | "exec": "npm-run-all dev:*", 44 | "delay": 500 45 | }, 46 | "author": "Brandin Canfield", 47 | "license": "MIT", 48 | "keywords": [ 49 | "Prisma", 50 | "TypeScript", 51 | "Next.js", 52 | "React" 53 | ], 54 | "dependencies": { 55 | "@prisma/client": "^5.1.1", 56 | "@prisma/internals": "^4.14.1", 57 | "@prisma/sdk": "^4.0.0", 58 | "@typescript-eslint/eslint-plugin": "^6.4.1", 59 | "@typescript-eslint/eslint-plugin-tslint": "^6.4.1", 60 | "@typescript-eslint/parser": "^6.4.1", 61 | "chalk": "^4.1.2", 62 | "cli-progress": "^3.12.0", 63 | "commander": "^11.0.0", 64 | "eslint": "^8.47.0", 65 | "eslint-config-next": "^13.4.19", 66 | "eslint-plugin-import": "^2.28.1", 67 | "eslint-plugin-prettier": "^4.2.1", 68 | "eslint-plugin-react": "^7.33.2", 69 | "eslint-plugin-react-hooks": "^4.6.0", 70 | "eslint-plugin-unused-imports": "^3.0.0", 71 | "figlet": "^1.6.0", 72 | "fs-extra": "^11.1.1", 73 | "nexquik": "^3.2.0", 74 | "next": "^13.4.19", 75 | "ora": "^5.4.1", 76 | "prettier": "^2.8.8", 77 | "ts-toolbelt": "^9.6.0" 78 | }, 79 | "devDependencies": { 80 | "@babel/core": "^7.22.1", 81 | "@babel/preset-env": "^7.22.4", 82 | "@babel/preset-typescript": "^7.21.5", 83 | "@semantic-release/changelog": "^6.0.3", 84 | "@semantic-release/git": "^10.0.1", 85 | "@types/cli-progress": "^3.11.0", 86 | "@types/eslint": "^8.40.0", 87 | "@types/figlet": "^1.5.6", 88 | "@types/fs-extra": "^11.0.1", 89 | "@types/jest": "^29.5.1", 90 | "@types/node": "^20.2.5", 91 | "@types/prettier": "^2.7.2", 92 | "babel-jest": "^29.5.0", 93 | "copyfiles": "^2.4.1", 94 | "jest": "^29.5.0", 95 | "nodemon": "^2.0.22", 96 | "npm-run-all": "^4.1.5", 97 | "npm-watch": "^0.11.0", 98 | "prisma": "^5.1.1", 99 | "rimraf": "^5.0.1", 100 | "typescript": "^5.0.4" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/modelTree.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import { DMMF } from "@prisma/generator-helper"; 3 | import chalk from "chalk"; 4 | 5 | export interface ModelTree { 6 | modelName: string; 7 | parent?: DMMF.Model; 8 | model: DMMF.Model; 9 | children: ModelTree[]; 10 | uniqueIdentifierField: { name: string; type: string }[]; 11 | } 12 | 13 | export function getCompositeIdField(model: DMMF.Model): DMMF.PrimaryKey | null { 14 | return model.primaryKey; 15 | } 16 | 17 | export function getCompositeKeyFields( 18 | model: ModelTree 19 | ): { fieldName: string; fieldType: string }[] | null { 20 | const compositeKeyFields: { fieldName: string; fieldType: string }[] = []; 21 | 22 | for (const field of model.model.fields) { 23 | if (field.kind === "object" && !field.isList && field.relationFromFields) { 24 | const relatedModel = model.children.find( 25 | (child) => child.model.name === field.type 26 | ); 27 | if (relatedModel) { 28 | const relatedField = relatedModel.model.fields.find( 29 | (f) => 30 | field.relationFromFields && f.name === field.relationFromFields[0] 31 | ); 32 | 33 | if (relatedField) { 34 | compositeKeyFields.push({ 35 | fieldName: field.relationFromFields[0], 36 | fieldType: relatedField.type, 37 | }); 38 | } 39 | } 40 | } 41 | } 42 | 43 | if (compositeKeyFields.length >= 2) { 44 | return compositeKeyFields; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | export function createModelTree( 51 | dataModel: DMMF.Datamodel, 52 | excludedModels: string[], 53 | includedModels: string[] 54 | ): ModelTree[] { 55 | const models = dataModel.models; 56 | 57 | // Create a map of models for efficient lookup 58 | const modelMap: Record = {}; 59 | for (const model of models) { 60 | modelMap[model.name] = model; 61 | } 62 | // console.log({ modelMap }); 63 | 64 | const visitedModels: Set = new Set(); 65 | const modelTrees: ModelTree[] = []; 66 | 67 | // Function to recursively build the model tree 68 | function buildModelTree( 69 | model: DMMF.Model, 70 | parent?: DMMF.Model 71 | ): ModelTree | undefined { 72 | // If we detect a circular relationship, just stop digging down into child nodes 73 | if (visitedModels.has(model.name)) { 74 | // throw new Error(`Circular relationship detected in model: ${model.name}`); 75 | // console.log(`Circular relationship detected in model: ${model.name}`); 76 | return; 77 | } 78 | 79 | visitedModels.add(model.name); 80 | 81 | const childRelationships = model.fields.filter( 82 | (field) => field.kind === "object" && field.isList 83 | ); 84 | 85 | const children: ModelTree[] = []; 86 | for (const relationship of childRelationships) { 87 | const childModel = modelMap[relationship.type]; 88 | if (childModel) { 89 | const childNode = buildModelTree(childModel, model); 90 | if (childNode) { 91 | children.push(childNode); 92 | } 93 | } 94 | } 95 | 96 | visitedModels.delete(model.name); 97 | const fullUniqueIdField = model.fields.find((field) => field.isId === true); 98 | let uniqueIdFieldReturn: { name: string; type: string }[] = []; 99 | if (!fullUniqueIdField) { 100 | // Check for composite id field 101 | const compositePrimaryKey = getCompositeIdField(model); 102 | if (compositePrimaryKey) { 103 | // For each field in fields, find the actual field 104 | const actualFields = model.fields 105 | .filter((modelField) => 106 | compositePrimaryKey.fields.includes(modelField.name) 107 | ) 108 | .map((f) => ({ name: f.name, type: f.type })); 109 | uniqueIdFieldReturn = actualFields; 110 | } else { 111 | console.log( 112 | chalk.red( 113 | `Nexquik could not fund a unique ID field for Model: ${model.name}` 114 | ) 115 | ); 116 | return; 117 | } 118 | } else { 119 | uniqueIdFieldReturn.push({ 120 | name: fullUniqueIdField.name, 121 | type: fullUniqueIdField.type, 122 | }); 123 | } 124 | return { 125 | modelName: model.name, 126 | model: model, 127 | parent: parent, 128 | uniqueIdentifierField: uniqueIdFieldReturn, 129 | children, 130 | }; 131 | } 132 | 133 | // Filter out included or excluded models before beginning main loop to generate Tree 134 | let topLevelModels: DMMF.Model[] = []; 135 | if (includedModels.length > 0) { 136 | topLevelModels = dataModel.models.filter((m) => 137 | includedModels.includes(m.name) 138 | ); 139 | } else { 140 | topLevelModels = dataModel.models.filter( 141 | (m) => !excludedModels.includes(m.name) 142 | ); 143 | } 144 | for (const model of topLevelModels) { 145 | // Only include models that dont have required parent 146 | if ( 147 | !model.fields.some( 148 | (field) => field.kind === "object" && field.isRequired && !field.isList 149 | ) 150 | ) { 151 | const modelTree = buildModelTree(model); 152 | if (modelTree) { 153 | modelTrees.push(modelTree); 154 | } 155 | } 156 | } 157 | 158 | return modelTrees; 159 | } 160 | 161 | export function getParentReferenceField( 162 | modelTree: ModelTree 163 | ): string | undefined { 164 | if (!modelTree.parent) { 165 | return undefined; 166 | } 167 | 168 | const parentModel = modelTree.model; 169 | const parentField = parentModel.fields.find( 170 | (field) => field.type === modelTree.parent?.name 171 | ); 172 | 173 | if (!parentField) { 174 | return undefined; 175 | } 176 | 177 | // Find the unique ID field in the current model that matches the parent reference field 178 | const uniqueIdField = modelTree.model.fields.find( 179 | (field) => field.name === parentField.name 180 | ); 181 | 182 | return uniqueIdField?.name; 183 | } 184 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/g2/5cg9h7sx517f0_jh7rjw40jc0000gq/T/jest_dz", 15 | 16 | // Automatically clear mock calls, instances, contexts and results before every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: ["/node_modules/"], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | // coverageProvider: "v8", 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: undefined, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: undefined, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // The default configuration for fake timers 52 | // fakeTimers: { 53 | // "enableGlobally": false 54 | // }, 55 | 56 | // Force coverage collection from ignored files using an array of glob patterns 57 | // forceCoverageMatch: [], 58 | 59 | // A path to a module which exports an async function that is triggered once before all test suites 60 | // globalSetup: undefined, 61 | 62 | // A path to a module which exports an async function that is triggered once after all test suites 63 | // globalTeardown: undefined, 64 | 65 | // A set of global variables that need to be available in all test environments 66 | // globals: {}, 67 | 68 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 69 | // maxWorkers: "50%", 70 | 71 | // An array of directory names to be searched recursively up from the requiring module's location 72 | // moduleDirectories: [ 73 | // "node_modules" 74 | // ], 75 | 76 | // An array of file extensions your modules use 77 | // moduleFileExtensions: [ 78 | // "js", 79 | // "mjs", 80 | // "cjs", 81 | // "jsx", 82 | // "ts", 83 | // "tsx", 84 | // "json", 85 | // "node" 86 | // ], 87 | 88 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 89 | // moduleNameMapper: {}, 90 | 91 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 92 | // modulePathIgnorePatterns: [], 93 | 94 | // Activates notifications for test results 95 | // notify: false, 96 | 97 | // An enum that specifies notification mode. Requires { notify: true } 98 | // notifyMode: "failure-change", 99 | 100 | // A preset that is used as a base for Jest's configuration 101 | // preset: undefined, 102 | 103 | // Run tests from one or more projects 104 | // projects: undefined, 105 | 106 | // Use this configuration option to add custom reporters to Jest 107 | // reporters: undefined, 108 | 109 | // Automatically reset mock state before every test 110 | // resetMocks: false, 111 | 112 | // Reset the module registry before running each individual test 113 | // resetModules: false, 114 | 115 | // A path to a custom resolver 116 | // resolver: undefined, 117 | 118 | // Automatically restore mock state and implementation before every test 119 | // restoreMocks: false, 120 | 121 | // The root directory that Jest should scan for tests and modules within 122 | // rootDir: undefined, 123 | 124 | // A list of paths to directories that Jest should use to search for files in 125 | // roots: [ 126 | // "" 127 | // ], 128 | 129 | // Allows you to use a custom runner instead of Jest's default test runner 130 | // runner: "jest-runner", 131 | 132 | // The paths to modules that run some code to configure or set up the testing environment before each test 133 | // setupFiles: [], 134 | 135 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 136 | // setupFilesAfterEnv: [], 137 | 138 | // The number of seconds after which a test is considered as slow and reported as such in the results. 139 | // slowTestThreshold: 5, 140 | 141 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 142 | // snapshotSerializers: [], 143 | 144 | // The test environment that will be used for testing 145 | // testEnvironment: "jest-environment-node", 146 | 147 | // Options that will be passed to the testEnvironment 148 | // testEnvironmentOptions: {}, 149 | 150 | // Adds a location field to test results 151 | // testLocationInResults: false, 152 | 153 | // The glob patterns Jest uses to detect test files 154 | // testMatch: [ 155 | // "**/__tests__/**/*.[jt]s?(x)", 156 | // "**/?(*.)+(spec|test).[tj]s?(x)" 157 | // ], 158 | 159 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 160 | // testPathIgnorePatterns: [ 161 | // "/node_modules/" 162 | // ], 163 | 164 | // The regexp pattern or array of patterns that Jest uses to detect test files 165 | // testRegex: [], 166 | 167 | // This option allows the use of a custom results processor 168 | // testResultsProcessor: undefined, 169 | 170 | // This option allows use of a custom test runner 171 | // testRunner: "jest-circus/runner", 172 | 173 | // A map from regular expressions to paths to transformers 174 | // transform: undefined, 175 | 176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 177 | // transformIgnorePatterns: [ 178 | // "/node_modules/", 179 | // "\\.pnp\\.[^\\/]+$" 180 | // ], 181 | 182 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 183 | // unmockedModulePathPatterns: undefined, 184 | 185 | // Indicates whether each individual test should be reported during the run 186 | // verbose: undefined, 187 | 188 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 189 | // watchPathIgnorePatterns: [], 190 | 191 | // Whether to use watchman for file crawling 192 | watchman: true, 193 | testMatch: ["**/__tests__/**/*.test.ts"], 194 | }; 195 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { GeneratorOptions } from "@prisma/generator-helper"; 2 | import chalk from "chalk"; 3 | import { Command } from "commander"; 4 | import figlet from "figlet"; 5 | import { generate } from "./generators"; 6 | import { formatDirectory, installPackages } from "./helpers"; 7 | import path from "path"; 8 | import { spawnSync } from "child_process"; 9 | import { ESLint } from "eslint"; 10 | // require("eslint-plugin-unused-imports"); 11 | export interface CliArgs { 12 | prismaSchemaPath: string; 13 | outputDirectory: string; 14 | } 15 | export const defaultOutputDirectory = "./"; 16 | export interface Group { 17 | name: string; 18 | include: string[]; 19 | exclude: string[]; 20 | } 21 | const defaultPrismaSchemaPath = "schema.prisma"; 22 | 23 | export async function run(options?: GeneratorOptions) { 24 | try { 25 | console.log( 26 | chalk.bgYellow.blue.bold( 27 | figlet.textSync("Nexquik", { 28 | font: "Univers", 29 | horizontalLayout: "default", 30 | verticalLayout: "default", 31 | }) 32 | ) 33 | ); 34 | const program = new Command(); 35 | let deps = false; 36 | // Create an array to collect group objects 37 | const groups: Group[] = []; 38 | let currentGroup: 39 | | { name: string; include: string[]; exclude: string[] } 40 | | undefined = undefined; 41 | program 42 | .version(require("../package.json").version) 43 | .description("Auto-generate Next.js UI components from your DB Schema") 44 | .option( 45 | "--schema ", 46 | "Path to prisma schema file", 47 | defaultPrismaSchemaPath 48 | ) 49 | .option( 50 | "--output ", 51 | "Path to root directory of your project", 52 | defaultOutputDirectory 53 | ) 54 | 55 | .option("--init", "Initializes a full Next.js app from scratch") 56 | .option( 57 | "--extendOnly", 58 | "Only creates the models specified in the current command, and leaves previously created ones alone." 59 | ) 60 | .option( 61 | "--appTitle ", 62 | "Title to be used in the header of your app", 63 | "App" 64 | ) 65 | .option( 66 | "--rootName <dirName>", 67 | "Desired name for the root app dir for your generated groups (this is the first directory nested under your 'app' directory.", 68 | "gen" 69 | ) 70 | .option( 71 | "--depth <depthValue>", 72 | "Maximum recursion depth for your models. (Changing this for large data models is not recommended, unless you filter down your models with the 'include' or 'exclude' flags also.)", 73 | "5" 74 | ) 75 | .option( 76 | "--modelsOnly", 77 | "Only generates components for your models. Skips the boilerplate files - root page.tsx,layout.tsx, globals.css, etc...." 78 | ) 79 | .option( 80 | "--prismaImport <prismaImportString>", 81 | "Import location for your prisma client if it differs from the standard setup.", 82 | `import prisma from '@/lib/prisma';` 83 | ) 84 | .option("--disabled", "Disable the generator", false); 85 | 86 | program 87 | .command("group") 88 | .description( 89 | "Create a group to organize your models into route groups. You can use this command multiple times to create many groups" 90 | ) 91 | .option("--name <groupName>", "Specify a group name", (groupName) => { 92 | // Create a new group object for each group 93 | currentGroup = { name: groupName, include: [], exclude: [] }; 94 | groups.push(currentGroup); 95 | }) 96 | .option( 97 | "--include <modelNames>", 98 | "Specify included types (comma-separated)", 99 | (modelNames) => { 100 | // Add the included types to the current group 101 | if (currentGroup) { 102 | currentGroup.include = modelNames.split(","); 103 | } 104 | } 105 | ) 106 | .option( 107 | "--exclude <modelNames>", 108 | "Specify excluded types (comma-separated)", 109 | (modelNames) => { 110 | // Add the excluded types to the current group 111 | if (currentGroup) { 112 | currentGroup.exclude = modelNames.split(","); 113 | } 114 | } 115 | ); 116 | 117 | program 118 | .command("deps") 119 | .description( 120 | "Install nexquik dependencies and copy over required config files. (tailwind, postcss, auto-prefixer, etc)" 121 | ) 122 | .action(() => { 123 | console.log("deps in here"); 124 | deps = true; 125 | }); 126 | 127 | // If prisma generator, parse the cli args from the generator config 128 | if (options?.generator.config) { 129 | try { 130 | const genArgs = options?.generator.config.command.split(" ") || []; 131 | program.parse(genArgs, { from: "user" }); 132 | } catch { 133 | throw Error("Invalid args"); 134 | } 135 | } else { 136 | // Else, parse from cli args 137 | program.parse(process.argv); 138 | } 139 | 140 | const cliArgs = program.opts(); 141 | const prismaSchemaPath = options?.schemaPath || cliArgs.schema; 142 | const outputDirectory = cliArgs.output; 143 | const maxDepth = parseInt(cliArgs.Depth); 144 | const rootName = cliArgs.rootName; 145 | const prismaImportString = cliArgs.prismaImport; 146 | const init = cliArgs.init || false; 147 | const extendOnly = cliArgs.extendOnly || false; 148 | const modelsOnly = cliArgs.modelsOnly || false; 149 | const disabled = 150 | process.env.DISABLE_NEXQUIK === "true" || cliArgs.disabled === true; 151 | const appTitle = cliArgs.appTitle; 152 | if (disabled) { 153 | return console.log("Nexquik generation disabled due to env var"); 154 | } 155 | 156 | await generate( 157 | prismaSchemaPath, 158 | outputDirectory, 159 | maxDepth, 160 | init, 161 | rootName, 162 | groups, 163 | extendOnly, 164 | deps, 165 | prismaImportString, 166 | appTitle, 167 | modelsOnly 168 | ); 169 | 170 | if (!deps) { 171 | console.log(`${chalk.blue.bold("\nLinting Generated Files...")}`); 172 | try { 173 | const startTime = new Date().getTime(); 174 | const eslint = new ESLint({ 175 | fix: true, 176 | useEslintrc: false, 177 | overrideConfig: { 178 | extends: [ 179 | "plugin:@typescript-eslint/eslint-recommended", 180 | "plugin:@typescript-eslint/recommended", 181 | ], 182 | parser: "@typescript-eslint/parser", 183 | plugins: ["unused-imports"], 184 | rules: { 185 | "no-unused-vars": "off", 186 | "@typescript-eslint/no-unused-vars": "error", 187 | "unused-imports/no-unused-imports": "error", 188 | "import/no-unused-modules": ["error"], 189 | }, 190 | }, 191 | }); 192 | const results = await eslint.lintFiles([ 193 | `${outputDirectory}/app/${rootName}/**/*.tsx`, 194 | ]); 195 | 196 | await ESLint.outputFixes(results); 197 | const endTime = new Date().getTime(); 198 | const duration = (endTime - startTime) / 1000; 199 | console.log(chalk.gray(`(Linted in ${duration} seconds)`)); 200 | } catch { 201 | console.log( 202 | chalk.gray( 203 | `Info: Something weird occured when linting. This may happen when running via 'npx', and you don't have nexquik installed in your node modules.` 204 | ) 205 | ); 206 | } 207 | } 208 | console.log(`${chalk.green.bold("\n✔ Success!")}`); 209 | return; 210 | } catch (error) { 211 | console.log(chalk.red.bold("Nexquik Error:\n"), error); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <br /> 2 | 3 | <p align="center"> 4 | <img align=top src="https://github.com/bcanfield/nexquik/assets/12603953/91861aeb-f7ff-4830-aded-760730a1057b" alt="Logo" width="140" height="140"> 5 | <br><h1 align="center">Nexquik</h1> 6 | 7 | 8 | <p align="center"> 9 | <strong align="center"> 10 | Transform your Prisma Models into stunning Next.js UI Components in seconds. 11 | </strong> 12 | <h3 align="center"> 13 | <a href="#live-demo">Live Demo</a> 14 | </h3> 15 | </p> 16 | <p align="center"> 17 | Auto-generate pieces of your app, allowing you to focus on refining your more custom components. 18 | </p> 19 | <p align="center"> 20 | These will be <strong>Server Components</strong> fully equipped with <strong>Server Actions</strong> 21 | </p> 22 | </p> 23 | </p> 24 | 25 | <p align="center"> 26 | <a href="#usage">Usage</a> • 27 | <a href="#options">Options</a> • 28 | <a href="#use-cases">Use Cases</a> 29 | </p> 30 | 31 | ## Usage 32 | ### Example - Creating a new app from scratch 33 | *Create a full Next.js app from scratch using all of your models* 34 | ```zsh 35 | npm i nexquik -g 36 | ``` 37 | 38 | **Option 1: Add the generator to your Prisma Schema and run `prisma generate`** 39 | 40 | ```prisma 41 | generator nexquik { 42 | provider = "prisma-generator-nexquik" 43 | command = "--init group --name Main" 44 | } 45 | ``` 46 | **Option 2: Use the command line** 47 | ```zsh 48 | npx nexquik --init group --name Main 49 | ``` 50 | <br></br> 51 | ### Example - Initializing Nexquik in an existing app 52 | *Install nexquik, install dependencies and required files, and generate some selected models into the app directory.* 53 | ```zsh 54 | npm i nexquik 55 | ``` 56 | ```zsh 57 | nexquik deps 58 | ``` 59 | Now that we installed Nexquik and initialized it in the project, you can add the generator to your schema, and your UI will be generated every time you run `prisma generate`. 60 | 61 | 62 | ```prisma 63 | generator nexquik { 64 | provider = "prisma-generator-nexquik" 65 | command = "group --name UserManagement --include User,Admin,Organization group --name TaskManagement --include Task,Product,Category" 66 | } 67 | ``` 68 | 69 | Keeping the generator in your schema will ensure that any time your models change, your UI will reflect them. 70 | 71 | This also allows you to benefit from enhancements to the project from the open source community. 72 | <br></br> 73 | 74 | ## Options 75 | | Options | Description | 76 | |-------------------------------------|-----------------------------------------------------------------------------------------------------------------| 77 | | -V, --version | Output the version number | 78 | | --schema <schemaLocation> | Path to prisma schema file (default: "schema.prisma") | 79 | | --output <outputDir> | Path to root directory of your project (default: "./") | 80 | | --init | Initializes a full Next.js app from scratch | 81 | | --extendOnly | Only creates the models specified in the current command, and leaves previously created ones alone. | 82 | | --appTitle <title> | Title to be used in the header of your app (default: "App") | 83 | | --rootName <dirName> | Desired name for the root app dir for your generated groups (this is the first directory nested under your 'app' directory. (default: "gen") | 84 | | --depth <depthValue> | Maximum recursion depth for your models. (Changing this for large data models is not recommended, unless you filter down your models with the 'include' or 'exclude' flags also.) (default: "5") | 85 | | --modelsOnly | Only generates components for your models. Skips the boilerplate files - root page.tsx,layout.tsx, globals.css, etc.... | 86 | | --prismaImport <prismaImportString> | Import location for your prisma client if it differs from the standard setup. (default: "import prisma from '@/lib/prisma';") | 87 | | --disabled | Disable the generator (default: false) | 88 | | -h, --help | Display help for command | 89 | 90 | | Commands | Description | 91 | |-------------------------------------|-----------------------------------------------------------------------------------------------------------------| 92 | | group [options] | Create a group to organize your models into route groups. You can use this command multiple times to create many groups | 93 | | deps | Install nexquik dependencies and copy over required config files. (tailwind, postcss, auto-prefixer, etc) | 94 | | help [command] | Display help for command | 95 | 96 | ### Disabled 97 | To disable Nexquik from generating during a Prisma generate, you can either use the `--disabled` CLI option or set the following env var. 98 | ```zsh 99 | DISABLE_NEXQUIK=true 100 | ``` 101 | <br></br> 102 | ## Live Demo 103 | The live demo is hosted through [CodeSandbox](https://codesandbox.io/p/sandbox/github/bcanfield/nexquik/tree/main). 104 | 105 | In CodeSandbox, the `example` directory will contain your app to poke around in. 106 | 107 | The demo is configured to re-generate your app every time you make a change to your prisma schema. 108 | <br></br> 109 | ## Use Cases 110 | Portions of your app that rely on simple CRUD operations are prime candidates for auto-generation. Here are some examples. 111 | 112 | ### User Management 113 | A user management section typically involves creating, reading, updating, and deleting user accounts. This could include functionalities like user registration, profile management, password reset, and account deletion. 114 | 115 | ### Admin Screens 116 | Admin screens often require CRUD operations to manage various aspects of the application or website. This could include managing content, users, roles, permissions, settings, and more. 117 | 118 | ### Product Catalog 119 | An e-commerce website's product catalog might involve creating, reading, updating, and deleting products. Admins could add new products, update product details, and remove products that are no longer available. 120 | 121 | ### Content Management System (CMS) 122 | In a CMS, content creators might need to perform CRUD operations on articles, blog posts, images, and other types of content. They can create, edit, delete, and publish content. 123 | 124 | ### Task Management 125 | For a task management app, users may need to perform CRUD operations on tasks. This includes adding new tasks, marking tasks as completed, updating task details, and deleting tasks. 126 | 127 | ### Customer Relationship Management (CRM) 128 | CRM systems require basic CRUD operations to manage customer information. Users can add new contacts, update contact details, log interactions, and delete contacts if needed. 129 | 130 | ### Event Calendar 131 | An event calendar app may involve CRUD operations for adding, updating, and deleting events. Users can create new events, edit event details, and remove events from the calendar. 132 | 133 | ### Inventory Management 134 | For an inventory management system, CRUD operations could be used to manage stock items. Users can add new items, update quantities, adjust prices, and mark items as discontinued. 135 | 136 | ### Feedback or Comment System 137 | Websites with user-generated content might need CRUD operations for handling feedback, comments, or reviews. Users can post new comments, edit their comments, and delete them. 138 | 139 | ### Polls or Surveys 140 | Poll or survey applications may involve CRUD operations to manage questions, options, and responses. Admins can create new polls, update question wording, and analyze collected responses. 141 | 142 | 143 | <br></br> 144 | <div align="center"> 145 | <!-- NPM version --> 146 | <a href="https://npmjs.org/package/nexquik"> 147 | <img src="https://img.shields.io/npm/dt/nexquik" 148 | alt="NPM version" /> 149 | </a> 150 | <!-- Build Status --> 151 | <a href="https://github.com/bcanfield/nexquik/actions/workflows/publish.yml"> 152 | <img src="https://github.com/bcanfield/nexquik/actions/workflows/publish.yml/badge.svg" 153 | alt="Build Status" /> 154 | </a> 155 | <!-- License --> 156 | <a href="https://npmjs.org/package/choo"> 157 | <img src="https://img.shields.io/badge/License-Apache%202.0-blue" 158 | alt="Download" /> 159 | </a> 160 | </div> 161 | 162 | -------------------------------------------------------------------------------- /src/templateRoot/app/rootGroupRouteLayout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import Link from "next/link"; 3 | import { Roboto } from "next/font/google"; 4 | import Image from "next/image"; 5 | import background from "./images/backdrop.png"; 6 | import logo from "./images/logo.png"; 7 | 8 | const roboto = Roboto({ 9 | weight: "400", 10 | subsets: ["latin"], 11 | display: "swap", 12 | }); 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | <html 20 | className={`${roboto.className} dark [--scroll-mt:9.875rem] lg:[--scroll-mt:6.3125rem] js-focus-visible`} 21 | > 22 | <body className="antialiased text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900"> 23 | {/* Start Backdrop Blur */} 24 | <div className="absolute z-20 top-0 inset-x-0 flex justify-center overflow-hidden pointer-events-none"> 25 | <div className="w-[108rem] flex-none flex justify-end"> 26 | <Image 27 | src={background} 28 | width={500} 29 | height={500} 30 | className="w-[90rem] flex-none max-w-none hidden dark:block" 31 | alt="background" 32 | /> 33 | </div> 34 | </div> 35 | {/* End Backdrop Blur */} 36 | {/* Navbar */} 37 | <div className="sticky top-0 z-40 w-full backdrop-blur flex-none transition-colors duration-500 lg:z-50 lg:border-b lg:border-slate-900/10 dark:border-slate-50/[0.06] bg-white supports-backdrop-blur:bg-white/95 dark:bg-transparent"> 38 | <div className="max-w-8xl mx-auto"> 39 | <div className="py-4 border-b border-slate-900/10 lg:px-8 lg:border-0 dark:border-slate-300/10 mx-4 lg:mx-0"> 40 | <div className="relative flex items-center"> 41 | <a className="flex items-center justify-between"> 42 | <span className="box-sizing: border-box; display: inline-block; overflow: hidden; width: initial; height: initial; background: none; opacity: 1; border: 0px; margin: 0px; padding: 0px; position: relative; max-width: 100%;"> 43 | <Image 44 | alt="Nexquik Logo" 45 | src={logo} 46 | width="35" 47 | height="35" 48 | ></Image> 49 | </span> 50 | <span className="self-center text-2xl ml-2 font-semibold whitespace-nowrap dark:text-white"> 51 | {/* @nexquik appTitle start */} 52 | App 53 | {/* @nexquik appTitle stop */} 54 | </span> 55 | </a> 56 | 57 | <div className="relative hidden lg:flex items-center ml-auto"> 58 | <nav className="text-sm leading-6 font-semibold text-slate-700 dark:text-slate-200"> 59 | <ul className="flex space-x-8"> 60 | <li> 61 | <a 62 | className="hover:text-sky-500 dark:hover:text-sky-400" 63 | href="https://nextjs.org/docs" 64 | > 65 | Next.js 66 | </a> 67 | </li> 68 | <li> 69 | <a 70 | className="hover:text-sky-500 dark:hover:text-sky-400" 71 | href="https://tailwindcss.com/docs/installation" 72 | > 73 | Tailwind CSS 74 | </a> 75 | </li> 76 | <li> 77 | <a 78 | className="hover:text-sky-500 dark:hover:text-sky-400" 79 | href="https://www.prisma.io/docs" 80 | > 81 | Prisma 82 | </a> 83 | </li> 84 | </ul> 85 | </nav> 86 | <div className="flex items-center border-l border-slate-200 ml-6 pl-6 dark:border-slate-800"> 87 | <a 88 | href="https://github.com/bcanfield/nexquik/stargazers" 89 | className="block text-slate-400 hover:text-slate-500 dark:hover:text-slate-300" 90 | > 91 | <span className="sr-only">Nexquik on GitHub</span> 92 | <svg 93 | viewBox="0 0 22 20" 94 | className="w-5 h-5" 95 | fill="currentColor" 96 | aria-hidden="true" 97 | > 98 | <path 99 | className="fill-slate-400 dark:fill-slate-500" 100 | d="M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z" 101 | />{" "} 102 | </svg> 103 | </a> 104 | <a 105 | href="https://github.com/bcanfield/nexquik" 106 | className="ml-6 block text-slate-400 hover:text-slate-500 dark:hover:text-slate-300" 107 | > 108 | <span className="sr-only">Nexquik on GitHub</span> 109 | <svg 110 | viewBox="0 0 16 16" 111 | className="w-5 h-5" 112 | fill="currentColor" 113 | aria-hidden="true" 114 | > 115 | <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path> 116 | </svg> 117 | </a> 118 | </div> 119 | </div> 120 | </div> 121 | </div> 122 | </div> 123 | <div className="sticky bottom-0 h-px -mt-px bg-slate-200 dark:bg-slate-400/20"></div> 124 | </div> 125 | {/* End Navbar */} 126 | {/* Page Content Start*/} 127 | <div className="w-full px-4 mx-auto max-w-8xl "> 128 | <div className="lg:flex"> 129 | {/* Side Nav */} 130 | 131 | <aside className="fixed inset-0 z-20 flex-none hidden h-full w-72 lg:static lg:h-auto lg:overflow-y-visible lg:pt-0 lg:w-[17rem] lg:block"> 132 | <div 133 | id="navWrapper" 134 | className="overflow-y-auto z-20 h-full bg-white scrolling-touch max-w-2xs lg:h-[calc(100vh-3rem)] lg:block lg:sticky top:24 dark:bg-slate-900 lg:mr-0 " 135 | > 136 | <nav 137 | id="nav" 138 | className="pt-16 px-1 pl-3 lg:pl-0 lg:pt-2 font-normal text-base lg:text-sm pb-10 lg:pb-20 sticky?lg:h-(screen-18)" 139 | > 140 | <ul className="mb-0 list-unstyled"> 141 | <li className=" dark:border-slate-400/20 mt-8"> 142 | <h2 className="pl-2 mb-4 lg:mb-1 font-semibold text-sm uppercase text-slate-900 dark:text-slate-200"> 143 | Route Groups 144 | </h2> 145 | </li> 146 | {/* //@nexquik routeSidebar start */} 147 | {/* //@nexquik routeSidebar stop */} 148 | </ul> 149 | </nav> 150 | </div> 151 | </aside> 152 | <div 153 | id="sidebarBackdrop" 154 | className="fixed inset-0 z-10 hidden bg-gray-900/50 dark:bg-gray-900/60" 155 | ></div> 156 | {/* End Side Nav */} 157 | 158 | <main 159 | id="content-wrapper" 160 | className="flex-auto w-full min-w-0 lg:static lg:max-h-full lg:overflow-visible " 161 | > 162 | {/* CHILDREN START */} 163 | 164 | <div className="flex w-full ">{children}</div> 165 | {/* CHILDREN END */} 166 | </main> 167 | 168 | {/* </div> */} 169 | </div> 170 | </div> 171 | {/* Page Content End*/} 172 | <footer className="text-sm leading-6 mt-16"> 173 | <div className="pt-10 pb-28 border-t border-slate-200 sm:flex justify-between text-slate-500 dark:border-slate-200/5 px-4"> 174 | <div className="mb-6 sm:mb-0 sm:flex"> 175 | <p>Generated by Nexquik</p> 176 | <p className="sm:ml-4 sm:pl-4 sm:border-l sm:border-slate-200 dark:sm:border-slate-200/5"> 177 | <a 178 | className="hover:text-slate-900 dark:hover:text-slate-400" 179 | href="https://apache.org/licenses/LICENSE-2.0" 180 | > 181 | License 182 | </a> 183 | </p> 184 | </div> 185 | <div className="flex space-x-10 text-slate-400 dark:text-slate-500"> 186 | <a 187 | href="https://github.com/bcanfield/nexquik" 188 | className="hover:text-slate-500 dark:hover:text-slate-400" 189 | > 190 | <span className="sr-only">GitHub</span> 191 | <svg width="25" height="24" fill="currentColor"> 192 | <path 193 | fillRule="evenodd" 194 | clipRule="evenodd" 195 | d="M12.846 0c-6.63 0-12 5.506-12 12.303 0 5.445 3.435 10.043 8.205 11.674.6.107.825-.262.825-.585 0-.292-.015-1.261-.015-2.291-3.015.569-3.795-.754-4.035-1.446-.135-.354-.72-1.446-1.23-1.738-.42-.23-1.02-.8-.015-.815.945-.015 1.62.892 1.845 1.261 1.08 1.86 2.805 1.338 3.495 1.015.105-.8.42-1.338.765-1.645-2.67-.308-5.46-1.37-5.46-6.075 0-1.338.465-2.446 1.23-3.307-.12-.308-.54-1.569.12-3.26 0 0 1.005-.323 3.3 1.26.96-.276 1.98-.415 3-.415s2.04.139 3 .416c2.295-1.6 3.3-1.261 3.3-1.261.66 1.691.24 2.952.12 3.26.765.861 1.23 1.953 1.23 3.307 0 4.721-2.805 5.767-5.475 6.075.435.384.81 1.122.81 2.276 0 1.645-.015 2.968-.015 3.383 0 .323.225.707.825.585a12.047 12.047 0 0 0 5.919-4.489 12.537 12.537 0 0 0 2.256-7.184c0-6.798-5.37-12.304-12-12.304Z" 196 | ></path> 197 | </svg> 198 | </a> 199 | </div> 200 | </div> 201 | </footer> 202 | </body> 203 | </html> 204 | ); 205 | } 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016-2022 Kong Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.2.1](https://github.com/bcanfield/nexquik/compare/v3.2.0...v3.2.1) (2023-10-13) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * Depdency Upgrades ([a1d6d94](https://github.com/bcanfield/nexquik/commit/a1d6d94d5bfbc0decbd2b4b949edd6c24824cb35)) 7 | 8 | # [3.2.0](https://github.com/bcanfield/nexquik/compare/v3.1.5...v3.2.0) (2023-09-16) 9 | 10 | 11 | ### Features 12 | 13 | * Fix for improper parent id handling ([169de3b](https://github.com/bcanfield/nexquik/commit/169de3b1f4b3e273ecc1976d106d1dc820783a14)) 14 | 15 | ## [3.1.5](https://github.com/bcanfield/nexquik/compare/v3.1.4...v3.1.5) (2023-09-05) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * Add Step attribute to Float html inputs ([4722f91](https://github.com/bcanfield/nexquik/commit/4722f91e8a5bf0ce44e578ccece746e371d2c5c5)) 21 | * Fix 'step' attribute on html input for Floats ([1034c08](https://github.com/bcanfield/nexquik/commit/1034c08f6aac3e6438d24ddccc08614ef0fe3f67)) 22 | 23 | ## [3.1.4](https://github.com/bcanfield/nexquik/compare/v3.1.3...v3.1.4) (2023-09-05) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * Converting optional parent reference to prisma create input ([2b91e70](https://github.com/bcanfield/nexquik/commit/2b91e70e21730657cf6ff2f38e681f1114b6bc97)) 29 | 30 | ## [3.1.3](https://github.com/bcanfield/nexquik/compare/v3.1.2...v3.1.3) (2023-09-05) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * Models Only CLI Option ([601e96e](https://github.com/bcanfield/nexquik/commit/601e96e380731a4c5bce6f7fcb0190193c951936)) 36 | 37 | ## [3.1.2](https://github.com/bcanfield/nexquik/compare/v3.1.1...v3.1.2) (2023-09-01) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * Update Readme and CLI Option descriptions ([1f6d26b](https://github.com/bcanfield/nexquik/commit/1f6d26b195887f959ec1732d533cd4d0bb5c1bf7)) 43 | 44 | ## [3.1.1](https://github.com/bcanfield/nexquik/compare/v3.1.0...v3.1.1) (2023-09-01) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * Catch exception in linting ([#89](https://github.com/bcanfield/nexquik/issues/89)) ([aef4724](https://github.com/bcanfield/nexquik/commit/aef47245728d06ce747c0d5085c2dd24d74ac06a)) 50 | 51 | # [3.1.0](https://github.com/bcanfield/nexquik/compare/v3.0.0...v3.1.0) (2023-08-31) 52 | 53 | 54 | ### Features 55 | 56 | * Installing Deps in existing Next.js project ([#88](https://github.com/bcanfield/nexquik/issues/88)) ([ce2f8de](https://github.com/bcanfield/nexquik/commit/ce2f8deaf5a9ad7d747787ab27098079f78c4749)) 57 | 58 | # [3.0.0](https://github.com/bcanfield/nexquik/compare/v2.2.2...v3.0.0) (2023-08-30) 59 | 60 | 61 | ### Features 62 | 63 | * Integrate into continuous workflow of existing Next.js projects ([#84](https://github.com/bcanfield/nexquik/issues/84)) ([bc2f137](https://github.com/bcanfield/nexquik/commit/bc2f1372ca2ec0823cbcedcf6509e96a25ecc8b8)) 64 | 65 | 66 | ### BREAKING CHANGES 67 | 68 | * Re-structuring the tool to better fit into the continuous workflow of existing apps 69 | 70 | ### Bug Fixes 71 | 72 | * Add tests for cli args ([a7d84fd](https://github.com/bcanfield/nexquik/commit/a7d84fd95c37b681df39b28200faa3e60e1b414a)) 73 | 74 | ## [2.2.2](https://github.com/bcanfield/nexquik/compare/v2.2.1...v2.2.2) (2023-08-21) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * Models only ([fd10673](https://github.com/bcanfield/nexquik/commit/fd106731e7c3c661aeed5a449e530deef13f4bbb)) 80 | * Prevent app dir generation from being called twice ([d989fa2](https://github.com/bcanfield/nexquik/commit/d989fa22d1b31627f2a06dd72656b1dd08ae5fb6)) 81 | 82 | ## [2.2.1](https://github.com/bcanfield/nexquik/compare/v2.2.0...v2.2.1) (2023-08-21) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * Remove nexquik .tar dependency ([6cfbcf3](https://github.com/bcanfield/nexquik/commit/6cfbcf39955d031d6c78db87d4c73bcd151a05f8)) 88 | * Testing and Organization Improvements ([#69](https://github.com/bcanfield/nexquik/issues/69)) ([2fa6e3d](https://github.com/bcanfield/nexquik/commit/2fa6e3ddf44ab9cf5462b2e280c9912784a7404f)) 89 | 90 | # [2.2.0](https://github.com/bcanfield/nexquik/compare/v2.1.3...v2.2.0) (2023-08-17) 91 | 92 | 93 | ### Features 94 | 95 | * Generation Output mode ([#68](https://github.com/bcanfield/nexquik/issues/68)) ([e848086](https://github.com/bcanfield/nexquik/commit/e848086a9aba25f2173e375a1f67a01ed812a0f9)) 96 | 97 | ## [2.1.3](https://github.com/bcanfield/nexquik/compare/v2.1.2...v2.1.3) (2023-08-17) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * Copy over tsconfig ([0d71a24](https://github.com/bcanfield/nexquik/commit/0d71a24b4b563c5f489236042c6ce3846bb712d9)) 103 | 104 | ## [2.1.2](https://github.com/bcanfield/nexquik/compare/v2.1.1...v2.1.2) (2023-08-17) 105 | 106 | 107 | ### Bug Fixes 108 | 109 | * Create prisma directory ([41bb7fd](https://github.com/bcanfield/nexquik/commit/41bb7fd99363e6543ec61899e1a2a62ee9aede03)) 110 | 111 | ## [2.1.1](https://github.com/bcanfield/nexquik/compare/v2.1.0...v2.1.1) (2023-08-17) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * Add option to disable generator via env var ([9bc9ee7](https://github.com/bcanfield/nexquik/commit/9bc9ee7d261c28188281121a97fcc50e7f176252)) 117 | * Reduce bundle size ([1a38b2a](https://github.com/bcanfield/nexquik/commit/1a38b2ae997b42f30839fa6dc6371d054237a370)) 118 | 119 | # [2.1.0](https://github.com/bcanfield/nexquik/compare/v2.0.6...v2.1.0) (2023-08-17) 120 | 121 | 122 | ### Features 123 | 124 | * Optimize file IO and add max-depth option ([#67](https://github.com/bcanfield/nexquik/issues/67)) ([540af5a](https://github.com/bcanfield/nexquik/commit/540af5a9795d87798e72b1aafd660dc30c42b4dd)) 125 | 126 | ## [2.0.6](https://github.com/bcanfield/nexquik/compare/v2.0.5...v2.0.6) (2023-08-16) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * Remove linting step to reduce execution time ([82605c4](https://github.com/bcanfield/nexquik/commit/82605c491a3d8d6aacfdc987126e159392bcbfaf)) 132 | 133 | ## [2.0.5](https://github.com/bcanfield/nexquik/compare/v2.0.4...v2.0.5) (2023-08-16) 134 | 135 | 136 | ### Bug Fixes 137 | 138 | * CLI Args ([b74f4d1](https://github.com/bcanfield/nexquik/commit/b74f4d1fe92f5912915620f53ac24b1b787ecea2)) 139 | 140 | ## [2.0.4](https://github.com/bcanfield/nexquik/compare/v2.0.3...v2.0.4) (2023-08-16) 141 | 142 | 143 | ### Bug Fixes 144 | 145 | * Dependency Upgrades ([6804a36](https://github.com/bcanfield/nexquik/commit/6804a36b54e0d4618e874e78a954897d834de010)) 146 | 147 | ## [2.0.3](https://github.com/bcanfield/nexquik/compare/v2.0.2...v2.0.3) (2023-08-16) 148 | 149 | 150 | ### Bug Fixes 151 | 152 | * Consistend CLI args ([ef0f1c2](https://github.com/bcanfield/nexquik/commit/ef0f1c23adae3fd91e00e4f2619602b866d0027a)) 153 | * Consistent CLI options ([6846d6d](https://github.com/bcanfield/nexquik/commit/6846d6d8526995a02785955ddeadcef8eabb20d3)) 154 | * Dependencies ([a4b2306](https://github.com/bcanfield/nexquik/commit/a4b23067fab60b4860e722276bba3a55da8fa255)) 155 | * Tests ([56729e5](https://github.com/bcanfield/nexquik/commit/56729e5f5142be1eaaad8cafdd94255551c5078c)) 156 | 157 | ## [2.0.2](https://github.com/bcanfield/nexquik/compare/v2.0.1...v2.0.2) (2023-08-16) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * CLI Arg parse ([d45732c](https://github.com/bcanfield/nexquik/commit/d45732c6c6daf606b9935d6cf27fd879407aa86f)) 163 | 164 | ## [2.0.1](https://github.com/bcanfield/nexquik/compare/v2.0.0...v2.0.1) (2023-08-15) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * Binary commands and logs ([ad3304d](https://github.com/bcanfield/nexquik/commit/ad3304d595ab21f84986109764f6fdc27c8f45dd)) 170 | * Tests ([8230266](https://github.com/bcanfield/nexquik/commit/82302668ba3aba34dd8ad778d5de7c0f8ee45f98)) 171 | 172 | # [2.0.0](https://github.com/bcanfield/nexquik/compare/v1.2.8...v2.0.0) (2023-08-15) 173 | 174 | 175 | ### Bug Fixes 176 | 177 | * Fix binaries ([ccd60d1](https://github.com/bcanfield/nexquik/commit/ccd60d1f0be1183581197edbaa3dafec8374f436)) 178 | * Remove nexquik dependency that is only used in dev env ([31ba559](https://github.com/bcanfield/nexquik/commit/31ba5596d833d21f98e1d2899083b7c928366918)) 179 | 180 | 181 | ### Features 182 | 183 | * V2 ([#65](https://github.com/bcanfield/nexquik/issues/65)) ([9a34b77](https://github.com/bcanfield/nexquik/commit/9a34b77ec0c8c5d7f07c29a025b6f248a6f2c8e4)) 184 | 185 | 186 | ### BREAKING CHANGES 187 | 188 | * V2 Improvements 189 | 190 | ## [1.2.8](https://github.com/bcanfield/nexquik/compare/v1.2.7...v1.2.8) (2023-07-24) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * Fix template app directory name ([#58](https://github.com/bcanfield/nexquik/issues/58)) ([54cf27f](https://github.com/bcanfield/nexquik/commit/54cf27f98cc7c0784f7e71a418a3f0eed64b487a)) 196 | 197 | ## [1.2.7](https://github.com/bcanfield/nexquik/compare/v1.2.6...v1.2.7) (2023-05-30) 198 | 199 | ### Bug Fixes 200 | 201 | - Body Styling fix ([669d2d3](https://github.com/bcanfield/nexquik/commit/669d2d31d55988d812faf000ecf901b6d838a132)) 202 | 203 | ## [1.2.6](https://github.com/bcanfield/nexquik/compare/v1.2.5...v1.2.6) (2023-05-30) 204 | 205 | ### Bug Fixes 206 | 207 | - Styling and bugfixes related to prisma import ([#11](https://github.com/bcanfield/nexquik/issues/11)) ([8dab68c](https://github.com/bcanfield/nexquik/commit/8dab68cc28b5af105009e25ccee5e3ab0f93a86a)) 208 | 209 | ## [1.2.5](https://github.com/bcanfield/nexquik/compare/v1.2.4...v1.2.5) (2023-05-30) 210 | 211 | ### Bug Fixes 212 | 213 | - Dependency Upgrade ([d53b571](https://github.com/bcanfield/nexquik/commit/d53b571e7629b3d65e754cd34fbe73633b039440)) 214 | 215 | ## [1.2.4](https://github.com/bcanfield/nexquik/compare/v1.2.3...v1.2.4) (2023-05-29) 216 | 217 | ### Bug Fixes 218 | 219 | - Dependencies ([219494b](https://github.com/bcanfield/nexquik/commit/219494badc0976fe8b227997fa741c73463962cb)) 220 | 221 | ## [1.2.3](https://github.com/bcanfield/nexquik/compare/v1.2.2...v1.2.3) (2023-05-29) 222 | 223 | ### Bug Fixes 224 | 225 | - Dependencies ([3640861](https://github.com/bcanfield/nexquik/commit/364086138b40ab719d5e67aaf889af07c996406e)) 226 | - Template Styling fixes and bugfixes ([#5](https://github.com/bcanfield/nexquik/issues/5)) ([1b211d1](https://github.com/bcanfield/nexquik/commit/1b211d1fe404f8d35a5df46627ea473634cfeabe)) 227 | 228 | ## [1.2.2](https://github.com/bcanfield/nexquik/compare/v1.2.1...v1.2.2) (2023-05-26) 229 | 230 | ### Bug Fixes 231 | 232 | - Add jest tests ([#4](https://github.com/bcanfield/nexquik/issues/4)) ([fbf3ff2](https://github.com/bcanfield/nexquik/commit/fbf3ff29fc26a6edc0b5fc440140eb1de790004a)) 233 | 234 | ## [1.2.1](https://github.com/bcanfield/nexquik/compare/v1.2.0...v1.2.1) (2023-05-26) 235 | 236 | ### Bug Fixes 237 | 238 | - Remove dist from release assets ([9a6403b](https://github.com/bcanfield/nexquik/commit/9a6403be0334a8f6213d0dd9fddc92a7022a0d7c)) 239 | 240 | # [1.2.0](https://github.com/bcanfield/nexquik/compare/v1.1.0...v1.2.0) (2023-05-26) 241 | 242 | ### Features 243 | 244 | - Initial generation of app dir ([#3](https://github.com/bcanfield/nexquik/issues/3)) ([20be668](https://github.com/bcanfield/nexquik/commit/20be6689bb314d52e8589bb47c18ef173aca2316)) 245 | 246 | # [1.1.0](https://github.com/bcanfield/nexquik/compare/v1.0.3...v1.1.0) (2023-05-12) 247 | 248 | ### Bug Fixes 249 | 250 | - Dependencies ([7898227](https://github.com/bcanfield/nexquik/commit/789822797645099304457d4daf44e71a8ea97bc1)) 251 | - Dependencies ([b793e95](https://github.com/bcanfield/nexquik/commit/b793e954fe4554735bb587a8810c7ae348f81b98)) 252 | 253 | ### Features 254 | 255 | - Prisma schema inspection and initial directory and file generation ([#2](https://github.com/bcanfield/nexquik/issues/2)) ([4218db9](https://github.com/bcanfield/nexquik/commit/4218db9a1fcae3b2f20cb9f7cccc628ab56572c0)) 256 | 257 | ## [1.0.3](https://github.com/bcanfield/prisnext/compare/v1.0.2...v1.0.3) (2023-05-11) 258 | 259 | ### Bug Fixes 260 | 261 | - Version increment ([48fd986](https://github.com/bcanfield/prisnext/commit/48fd98630ddfc3a3370962ca2e3b458fbe237589)) 262 | 263 | ## [1.0.2](https://github.com/bcanfield/prisnext/compare/v1.0.1...v1.0.2) (2023-05-11) 264 | 265 | ### Bug Fixes 266 | 267 | - Workflow ([236486d](https://github.com/bcanfield/prisnext/commit/236486d4d0b54b76f10050fceffb2c876ebc067d)) 268 | 269 | ## [1.0.1](https://github.com/bcanfield/prisnext/compare/v1.0.0...v1.0.1) (2023-05-11) 270 | 271 | ### Bug Fixes 272 | 273 | - Workflow ([9dcd379](https://github.com/bcanfield/prisnext/commit/9dcd37945f77c5c67952f857bf69e94e337ba039)) 274 | 275 | # 1.0.0 (2023-05-11) 276 | 277 | ### Bug Fixes 278 | 279 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c)) 280 | - Workflow ([986e448](https://github.com/bcanfield/prisnext/commit/986e44832e947137902ed9c40d553caffd2b07e1)) 281 | - Workflow ([79156d8](https://github.com/bcanfield/prisnext/commit/79156d8182cd027391498456a3b42a4cdff215b5)) 282 | - Workflow ([fbdb50e](https://github.com/bcanfield/prisnext/commit/fbdb50eb0a34ef5de33e5d43f080718e1229a718)) 283 | 284 | ### Features 285 | 286 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17)) 287 | 288 | # 1.0.0 (2023-05-11) 289 | 290 | ### Bug Fixes 291 | 292 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c)) 293 | - Workflow ([79156d8](https://github.com/bcanfield/prisnext/commit/79156d8182cd027391498456a3b42a4cdff215b5)) 294 | - Workflow ([fbdb50e](https://github.com/bcanfield/prisnext/commit/fbdb50eb0a34ef5de33e5d43f080718e1229a718)) 295 | 296 | ### Features 297 | 298 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17)) 299 | 300 | # 1.1.1 (2023-05-11) 301 | 302 | ### Bug Fixes 303 | 304 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c)) 305 | - Workflow ([fbdb50e](https://github.com/bcanfield/prisnext/commit/fbdb50eb0a34ef5de33e5d43f080718e1229a718)) 306 | 307 | ### Features 308 | 309 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17)) 310 | 311 | # 1.0.0 (2023-05-11) 312 | 313 | ### Bug Fixes 314 | 315 | - Remove dist ([61c163e](https://github.com/bcanfield/prisnext/commit/61c163eeae51eccfed0f60d0db4544c72656e26c)) 316 | 317 | ### Features 318 | 319 | - Initial CLI Setup ([#1](https://github.com/bcanfield/prisnext/issues/1)) ([aeb32ba](https://github.com/bcanfield/prisnext/commit/aeb32ba027944e78d756cec35fdb69cdc2e2ea17)) 320 | 321 | ## [1.0.1](https://github.com/bcanfield/prisnext/compare/v1.0.0...v1.0.1) (2023-05-11) 322 | 323 | ### Bug Fixes 324 | 325 | - Troubleshoot workflow ([0ba8ccb](https://github.com/bcanfield/prisnext/commit/0ba8ccbd5e2ad5973dc555a9674af00255571400)) 326 | 327 | # 1.0.0 (2023-05-11) 328 | 329 | ### Bug Fixes 330 | 331 | - Add Readme ([7bd6115](https://github.com/bcanfield/prisnext/commit/7bd6115f8521eb095faee989589d61f5a520305f)) 332 | - Troubleshoot ([ac95d1c](https://github.com/bcanfield/prisnext/commit/ac95d1cb3feefeb7523b3151a4d9b0593965c92e)) 333 | - Troubleshoot workflow ([637c8f0](https://github.com/bcanfield/prisnext/commit/637c8f00031dc060ea6199c6708f551ea30f3cd6)) 334 | - Troubleshoot workflow ([1634868](https://github.com/bcanfield/prisnext/commit/1634868e43bec360022ed525dd7751eb2ed53ded)) 335 | - Troubleshoot workflow ([8ab54cc](https://github.com/bcanfield/prisnext/commit/8ab54cc28e8734e6623335ade4ccaaf38101a63e)) 336 | - Troubleshoot workflow ([e5a3df1](https://github.com/bcanfield/prisnext/commit/e5a3df162ad8c16bc9279b6ed70da22ac9942e1c)) 337 | - Troubleshoot workflow ([951c61e](https://github.com/bcanfield/prisnext/commit/951c61e7d46556e9f40f48024a480ac67c72fa29)) 338 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import prettier from "prettier"; 5 | import { RouteObject } from "./generators"; 6 | // import ora from "ora"; 7 | import { execSync } from "child_process"; 8 | import { ESLint } from "eslint"; 9 | 10 | interface PackageOptions { 11 | sourcePackageJson: string; 12 | destinationDirectory: string; 13 | } 14 | 15 | export function installPackages({ 16 | sourcePackageJson, 17 | destinationDirectory, 18 | }: PackageOptions) { 19 | const sourcePackageData = fs.readFileSync(sourcePackageJson, "utf8"); 20 | const sourcePackage = JSON.parse(sourcePackageData); 21 | 22 | const dependencies = sourcePackage.dependencies || {}; 23 | const devDependencies = sourcePackage.devDependencies || {}; 24 | 25 | const installDeps = (deps: Record<string, string>, type: string) => { 26 | const depArray = Object.keys(deps); 27 | if (depArray.length > 0) { 28 | const cmd = `npm install --quiet ${depArray.join( 29 | " " 30 | )} --prefix ${destinationDirectory} --${type}`; 31 | try { 32 | execSync(cmd); 33 | // console.log(`${type} installed successfully.`); 34 | } catch (error: any) { 35 | console.error(`Error installing ${type}: ${error.message}`); 36 | throw error; 37 | } 38 | } 39 | }; 40 | 41 | installDeps(dependencies, "save"); 42 | installDeps(devDependencies, "save-dev"); 43 | } 44 | 45 | export function copyAndRenameFile( 46 | sourceFilePath: string, 47 | destinationDirectory: string, 48 | newFileName: string 49 | ) { 50 | const destinationFilePath = path.join(destinationDirectory, newFileName); 51 | 52 | try { 53 | // Check if the destination file exists 54 | if (fs.existsSync(destinationFilePath)) { 55 | // Delete the existing file 56 | fs.unlinkSync(destinationFilePath); 57 | } 58 | 59 | // Copy the source file to the destination 60 | fs.copyFileSync(sourceFilePath, destinationFilePath); 61 | } catch (error) { 62 | console.error(`An error occurred in copyAndRenameFile: ${error}`); 63 | } 64 | } 65 | 66 | export async function listFilesInDirectory( 67 | directoryPath: string 68 | ): Promise<string[]> { 69 | const files: string[] = []; 70 | 71 | async function traverseDirectory(currentPath: string): Promise<void> { 72 | const entries = await fs.promises.readdir(currentPath, { 73 | withFileTypes: true, 74 | }); 75 | 76 | for (const entry of entries) { 77 | const fullPath = path.join(currentPath, entry.name); 78 | 79 | if (entry.isFile()) { 80 | files.push(fullPath); 81 | } else if (entry.isDirectory()) { 82 | await traverseDirectory(fullPath); 83 | } 84 | } 85 | } 86 | 87 | await traverseDirectory(directoryPath); 88 | return files; 89 | } 90 | 91 | export const copyDirectoryContents = async ( 92 | sourceDirectory: string, 93 | destinationDirectory: string 94 | ) => { 95 | if (!fs.existsSync(sourceDirectory)) { 96 | throw new Error(`Source directory "${sourceDirectory}" does not exist.`); 97 | } 98 | 99 | // Create the destination directory if it doesn't exist 100 | if (!fs.existsSync(destinationDirectory)) { 101 | fs.mkdirSync(destinationDirectory, { recursive: true }); 102 | } 103 | 104 | const files = await fs.promises.readdir(sourceDirectory); 105 | 106 | for (const file of files) { 107 | const sourcePath = path.join(sourceDirectory, file); 108 | const destinationPath = path.join(destinationDirectory, file); 109 | 110 | const stat = await fs.promises.stat(sourcePath); 111 | 112 | if (stat.isDirectory()) { 113 | // If it's a sub-directory, recursively copy its contents 114 | await copyDirectoryContents(sourcePath, destinationPath); 115 | } else { 116 | // If it's a file, copy it to the destination 117 | await fs.promises.copyFile(sourcePath, destinationPath); 118 | } 119 | } 120 | }; 121 | const waitForEvent = (emitter: any, event: any) => 122 | new Promise((resolve) => emitter.once(event, resolve)); 123 | 124 | export async function copyPublicDirectory( 125 | sourceDir: string, 126 | destinationDir: string, 127 | toReplace = false, 128 | skipChildDir?: string 129 | ) { 130 | // console.log( 131 | // chalk.yellowBright(`Copying directory: ${sourceDir} to ${destinationDir}`) 132 | // ); 133 | 134 | try { 135 | if (toReplace && fs.existsSync(destinationDir)) { 136 | fs.rmSync(destinationDir, { recursive: true }); 137 | } 138 | 139 | if (!fs.existsSync(destinationDir)) { 140 | fs.mkdirSync(destinationDir); 141 | } 142 | 143 | const files = fs.readdirSync(sourceDir, { withFileTypes: true }); 144 | // for (let index = 0; index < array.length; index++) { 145 | // const element = array[index]; 146 | 147 | // } 148 | 149 | for (let index = 0; index < files.length; index++) { 150 | const entry = files[index]; 151 | 152 | const file = entry.name; 153 | 154 | if (file === skipChildDir) { 155 | return; 156 | } 157 | 158 | const sourceFile = path.join(sourceDir, file); 159 | const destinationFile = path.join(destinationDir, file); 160 | 161 | if (entry.isDirectory()) { 162 | copyDirectory( 163 | sourceFile, 164 | destinationFile, 165 | toReplace, 166 | skipChildDir !== undefined ? [skipChildDir] : undefined 167 | ); 168 | } else { 169 | if (!fs.existsSync(destinationFile)) { 170 | // fse.copyFileSync(sourceFile, destinationFile); 171 | const srcStream = fs.createReadStream(sourceFile); 172 | await waitForEvent(srcStream, "ready"); 173 | const destStream = fs.createWriteStream(destinationFile); 174 | await waitForEvent(destStream, "ready"); 175 | const handleError = (err: any) => { 176 | throw new Error(err); 177 | }; 178 | srcStream.on("error", handleError); 179 | destStream.on("error", handleError); 180 | 181 | srcStream.pipe(destStream); 182 | await waitForEvent(srcStream, "end"); 183 | } 184 | } 185 | } 186 | } catch (error) { 187 | console.error( 188 | chalk.red("An error occurred in copyPublicDirectory:", error) 189 | ); 190 | } 191 | } 192 | 193 | export async function copyImage( 194 | sourceDir: string, 195 | sourceFile: string, 196 | destinationDir: string 197 | ) { 198 | try { 199 | if (!fs.existsSync(destinationDir)) { 200 | fs.mkdirSync(destinationDir); 201 | } 202 | const destinationFile = path.join(destinationDir, sourceFile); 203 | 204 | if (!fs.existsSync(destinationFile)) { 205 | // fse.copyFileSync(sourceFile, destinationFile); 206 | const srcStream = fs.createReadStream(path.join(sourceDir, sourceFile)); 207 | await waitForEvent(srcStream, "ready"); 208 | const destStream = fs.createWriteStream(destinationFile); 209 | await waitForEvent(destStream, "ready"); 210 | const handleError = (err: any) => { 211 | throw new Error(err); 212 | }; 213 | srcStream.on("error", handleError); 214 | destStream.on("error", handleError); 215 | 216 | srcStream.pipe(destStream); 217 | await waitForEvent(srcStream, "end"); 218 | } 219 | } catch (error) { 220 | console.error(chalk.red("An error occurred in copyImage:", error)); 221 | } 222 | } 223 | 224 | function addStringsBetweenComments( 225 | fileContent: string, 226 | insertData: Array<{ 227 | insertString: string; 228 | startComment: string; 229 | endComment: string; 230 | }> 231 | ): string { 232 | insertData.forEach(({ insertString, startComment, endComment }) => { 233 | while ( 234 | fileContent.includes(startComment) && 235 | fileContent.includes(endComment) 236 | ) { 237 | const startIndex = fileContent.indexOf(startComment); 238 | const endIndex = fileContent.indexOf(endComment) + endComment.length; 239 | const contentToRemove = fileContent.slice(startIndex, endIndex); 240 | fileContent = fileContent.replace(contentToRemove, insertString); 241 | } 242 | }); 243 | 244 | return fileContent; 245 | } 246 | 247 | export async function modifyFile( 248 | sourceFilePath: string, 249 | destinationFilePath: string, 250 | insertData: Array<{ 251 | insertString: string; 252 | startComment: string; 253 | endComment: string; 254 | }>, 255 | modelName?: string 256 | ) { 257 | try { 258 | const fileContent = fs.readFileSync(sourceFilePath, "utf8"); 259 | 260 | // Perform string replacements 261 | let modifiedContent = addStringsBetweenComments(fileContent, insertData); 262 | modifiedContent = await prettier.format(modifiedContent, { 263 | parser: "babel-ts", // Specify the parser according to your project's configuration 264 | }); 265 | if (modelName) { 266 | modifiedContent = findAndReplaceInFile( 267 | modifiedContent, 268 | "nexquikTemplateModel", 269 | modelName 270 | ); 271 | } 272 | const eslint = new ESLint({ fix: true }); 273 | 274 | // let lintedText = ""; 275 | // // // const files = a wait getFilePaths(directoryPath); 276 | // const results = await eslint.lintText(modifiedContent); 277 | // if ( 278 | // results.length > 0 279 | // // results[0].output?.includes("CreateParticipant(") 280 | // ) { 281 | // // console.log({ results: results[0].output }); 282 | // // console.log({ modifiedContent }); 283 | // lintedText = results[0].output || ""; 284 | // } 285 | // // const formatter = await eslint.loadFormatter("stylish"); 286 | // const resultText = await formatter.format(results); 287 | // Write the modified content to the destination file 288 | // await fs.promises.writeFile( 289 | // destinationFilePath, 290 | // lintedText || modifiedContent 291 | // ); 292 | await fs.promises.writeFile(destinationFilePath, modifiedContent); 293 | 294 | return; 295 | } catch (error) { 296 | console.error("An error occurred in modifyFile:", error); 297 | } 298 | } 299 | 300 | export function createNestedDirectory(directory: string) { 301 | const baseParts = directory.split(path.sep).filter((item) => item !== ""); 302 | 303 | let currentBasePath = ""; 304 | for (const part of baseParts) { 305 | currentBasePath = path.join(currentBasePath, part); 306 | if (!fs.existsSync(currentBasePath)) { 307 | fs.mkdirSync(currentBasePath); 308 | } 309 | } 310 | return; 311 | } 312 | // copy files from one directory to another 313 | export function copyDirectory( 314 | sourceDir: string, 315 | destinationDir: string, 316 | toReplace = true, 317 | skipChildDirs: string[] = [] // Change the parameter name and set it as an array of strings 318 | ): void { 319 | try { 320 | if (!fs.existsSync(destinationDir)) { 321 | createNestedDirectory(destinationDir); 322 | } 323 | 324 | const files = fs.readdirSync(sourceDir, { withFileTypes: true }); 325 | files.forEach((entry) => { 326 | const file = entry.name; 327 | 328 | if (skipChildDirs.includes(file)) { 329 | // Check if the file is in the skipChildDirs array 330 | return; 331 | } 332 | 333 | const sourceFile = path.join(sourceDir, file); 334 | const destinationFile = path.join(destinationDir, file); 335 | 336 | if (entry.isDirectory()) { 337 | copyDirectory(sourceFile, destinationFile, toReplace, skipChildDirs); 338 | } else { 339 | fs.copyFileSync(sourceFile, destinationFile); 340 | } 341 | }); 342 | } catch (error) { 343 | console.error(chalk.red("An error occurred in copyDirectory:", error)); 344 | } 345 | } 346 | 347 | export const formatNextJsFilesRecursively = async (directory: string) => { 348 | // Get a list of all files and directories in the current directory 349 | const entries = await fs.promises.readdir(directory); 350 | 351 | for (const entry of entries) { 352 | const entryPath = path.join(directory, entry); 353 | 354 | // Check if the entry is a file 355 | const isFile = (await fs.promises.stat(entryPath)).isFile(); 356 | 357 | if (isFile) { 358 | // Filter the file to include only Next.js files (e.g., .js, .jsx, .ts, .tsx) 359 | if (/\.(jsx?|tsx?)$/.test(path.extname(entry))) { 360 | const fileContents = await fs.promises.readFile(entryPath, "utf8"); 361 | 362 | // Format the file contents using Prettier 363 | const formattedContents = await prettier.format(fileContents, { 364 | parser: "babel-ts", // Specify the parser according to your project's configuration 365 | }); 366 | 367 | // Write the formatted contents back to the file 368 | await fs.promises.writeFile(entryPath, formattedContents); 369 | } 370 | } else { 371 | // If the entry is a directory, recursively call the function for that directory 372 | await formatNextJsFilesRecursively(entryPath); 373 | } 374 | } 375 | }; 376 | export const deleteDirectoryRecursively = async (directoryPath: string) => { 377 | if (!fs.existsSync(directoryPath)) { 378 | throw new Error(`Directory "${directoryPath}" does not exist.`); 379 | } 380 | 381 | const files = await fs.promises.readdir(directoryPath); 382 | 383 | for (const file of files) { 384 | const filePath = path.join(directoryPath, file); 385 | const stat = await fs.promises.stat(filePath); 386 | 387 | if (stat.isDirectory()) { 388 | // If it's a sub-directory, recursively delete it 389 | await deleteDirectoryRecursively(filePath); 390 | } else { 391 | // If it's a file, delete it 392 | await fs.promises.unlink(filePath); 393 | } 394 | } 395 | 396 | // Delete the empty directory 397 | await fs.promises.rmdir(directoryPath); 398 | }; 399 | async function getFilePaths(directoryPath: string): Promise<string[]> { 400 | const files: string[] = []; 401 | 402 | const processDirectory = async (dirPath: string) => { 403 | const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); 404 | 405 | for (const entry of entries) { 406 | const fullPath = path.join(dirPath, entry.name); 407 | 408 | if (entry.isDirectory()) { 409 | await processDirectory(fullPath); // Recursively process subdirectories 410 | } else if (entry.isFile()) { 411 | files.push(fullPath); // Add file path to the array 412 | } 413 | } 414 | }; 415 | 416 | await processDirectory(directoryPath); 417 | 418 | return files; 419 | } 420 | 421 | export async function formatDirectory(directoryPath: string): Promise<void> { 422 | // const list = fs.readdirSync(directoryPath); 423 | const eslint = new ESLint({ fix: true }); 424 | 425 | // const files = await getFilePaths(directoryPath); 426 | 427 | const results = await eslint.lintFiles([`${directoryPath}/**/*.tsx`]); 428 | await ESLint.outputFixes(results); 429 | 430 | // Format the files using Prettier 431 | // const prettierConfig = await prettier.resolveConfig(directoryPath); 432 | // await Promise.all( 433 | // results.map(async (result) => { 434 | // const filePath = result.filePath; 435 | // const fileContent = await fs.promises.readFile(filePath, "utf-8"); 436 | // // const formattedContent = prettier.format(fileContent, { 437 | // // ...prettierConfig, 438 | // // filepath: filePath, 439 | // // }); 440 | // await fs.promises.writeFile(filePath, fileContent, "utf-8"); 441 | // }) 442 | // ); 443 | } 444 | 445 | export function findAndReplaceInFile( 446 | textContent: string, 447 | searchString: string, 448 | replacementString: string 449 | ): string { 450 | const pattern = new RegExp(searchString, "gi"); 451 | return textContent.replace(pattern, (match) => { 452 | // Preserve the casing of the first character 453 | const firstChar = match.charAt(0); 454 | const replacementFirstChar = replacementString.charAt(0); 455 | const replacedFirstChar = 456 | firstChar === firstChar.toLowerCase() 457 | ? replacementFirstChar.toLowerCase() 458 | : firstChar === firstChar.toUpperCase() 459 | ? replacementFirstChar.toUpperCase() 460 | : replacementFirstChar; 461 | return replacedFirstChar + replacementString.slice(1); 462 | }); 463 | } 464 | 465 | export function copyFileToDirectory( 466 | sourceFilePath: string, 467 | targetDirectoryPath: string 468 | ): void { 469 | const fileName = path.basename(sourceFilePath); 470 | const targetFilePath = path.join(targetDirectoryPath, fileName); 471 | 472 | fs.copyFileSync(sourceFilePath, targetFilePath); 473 | } 474 | 475 | export function popStringEnd(str: string, char: string): string { 476 | const lastIndex = str.lastIndexOf(char); 477 | 478 | if (lastIndex === -1) { 479 | // Character not found in the string 480 | return str; 481 | } 482 | return str.substring(0, lastIndex); 483 | } 484 | 485 | export function prettyPrintAPIRoutes(routes: RouteObject[]) { 486 | console.log("API Routes:"); 487 | console.log("-----------"); 488 | for (const route of routes) { 489 | console.log( 490 | `${route.segment} - ${route.operation} ${route.model}: ${route.description}` 491 | ); 492 | } 493 | } 494 | export const getDynamicSlugs = ( 495 | modelName: string | undefined, 496 | uniqueIdFieldNames: string[] 497 | ): string[] => { 498 | const slugs: string[] = []; 499 | uniqueIdFieldNames.forEach((idField) => { 500 | slugs.push(`${modelName}${idField}`); 501 | }); 502 | return slugs; 503 | }; 504 | 505 | export function convertRouteToRedirectUrl(input: string): string { 506 | const regex = /\[(.*?)\]/g; 507 | const replaced = input.replace(regex, (_, innerValue) => { 508 | return `\${params.${innerValue}}`; 509 | }); 510 | 511 | return `${replaced}`; 512 | } 513 | export function convertRoutesToRedirectUrl(input: string): string { 514 | const regex = /\[(.*?)\]/g; 515 | const replaced = input.replace(regex, (_, innerValue) => { 516 | return `\${params.${innerValue}}`; 517 | }); 518 | 519 | return `${replaced}`; 520 | } 521 | -------------------------------------------------------------------------------- /src/generators.ts: -------------------------------------------------------------------------------- 1 | import { DMMF } from "@prisma/generator-helper"; 2 | import { getDMMF } from "@prisma/internals"; 3 | import chalk from "chalk"; 4 | import fs from "fs"; 5 | import * as fse from "fs-extra"; 6 | 7 | import path from "path"; 8 | 9 | import { promisify } from "util"; 10 | import { 11 | convertRouteToRedirectUrl, 12 | copyAndRenameFile, 13 | copyDirectory, 14 | copyImage, 15 | copyPublicDirectory, 16 | createNestedDirectory, 17 | getDynamicSlugs, 18 | installPackages, 19 | modifyFile, 20 | } from "./helpers"; 21 | import { 22 | ModelTree, 23 | createModelTree, 24 | getParentReferenceField, 25 | } from "./modelTree"; 26 | import { Group } from "./cli"; 27 | 28 | const blueButtonClass = 29 | "px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-sky-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-sky-600 dark:focus:ring-blue-500 dark:focus:text-white"; 30 | const grayButtonClass = 31 | "px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"; 32 | 33 | const blueTextClass = 34 | "dark:text-sky-400 hover:text-sky-500 dark:hover:text-sky-600"; 35 | 36 | const labelClass = "block text-slate-500 dark:text-slate-400 text-sm"; 37 | const inputClass = 38 | "accent-sky-700 block text-sm leading-6 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 hover:ring-slate-300 dark:bg-slate-800 dark:highlight-white/5 dark:hover:bg-slate-700"; 39 | const disabledInputClass = 40 | "block leading-6 text-slate-400 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 dark:bg-slate-800 "; 41 | const darkTextClass = "text-slate-700 dark:text-slate-400"; 42 | const lightTextClass = "text-slate-900 dark:text-slate-200"; 43 | const readFileAsync = promisify(fs.readFile); 44 | 45 | interface RouteSegment { 46 | segment: string; 47 | fullRoute: string; 48 | } 49 | 50 | function splitTheRoute(route: string): RouteSegment[] { 51 | const segments = route.split("/").filter((r) => r != ""); 52 | let currentRoute = ""; 53 | 54 | const returnSegs = segments.flatMap((segment) => { 55 | let newSegment = segment; 56 | if (segment) { 57 | if (segment.includes("[")) { 58 | currentRoute += "/$" + segment.replace(/\[(.*?)\]/g, "{params.$1}"); 59 | newSegment = segment.replace(/\[(.*?)\]/g, "{params.$1}"); 60 | } else { 61 | currentRoute += `/${segment}`; 62 | } 63 | return { segment: newSegment, fullRoute: currentRoute }; 64 | } 65 | return []; 66 | }); 67 | return returnSegs; 68 | } 69 | function generateBreadCrumb(route: string) { 70 | let routeCrumbs = ""; 71 | splitTheRoute(route).forEach(({ segment, fullRoute }, index) => { 72 | routeCrumbs += ` 73 | 74 | <li> 75 | <div className="flex items-center"> 76 | ${ 77 | index === 0 78 | ? `<svg className="w-3 h-3 mr-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> 79 | <path d="m19.707 9.293-2-2-7-7a1 1 0 0 0-1.414 0l-7 7-2 2a1 1 0 0 0 1.414 1.414L2 10.414V18a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 0 1 1h3a2 2 0 0 0 2-2v-7.586l.293.293a1 1 0 0 0 1.414-1.414Z"/> 80 | </svg>` 81 | : `<svg className="w-3 h-3 text-gray-400 mx-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> 82 | <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 9 4-4-4-4"/> 83 | </svg>` 84 | } 85 | 86 | <Link href={\`${fullRoute}\`} className="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2 dark:text-gray-400 dark:hover:text-white">${segment}</Link> 87 | </div> 88 | </li>`; 89 | }); 90 | const breadCrumb = ` 91 | <nav className="flex" aria-label="Breadcrumb"> 92 | <ol className="inline-flex items-center space-x-1 md:space-x-3"> 93 | 94 | ${routeCrumbs} 95 | 96 | </ol> 97 | </nav> 98 | 99 | `; 100 | return breadCrumb; 101 | } 102 | 103 | export const prismaFieldToInputType: Record<string, string> = { 104 | Int: "number", 105 | Float: "number", 106 | String: "text", 107 | Boolean: "checkbox", 108 | DateTime: "datetime-local", 109 | Json: "text", 110 | }; 111 | 112 | export interface RouteObject { 113 | segment: string; 114 | model: string; 115 | operation: string; 116 | description: string; 117 | } 118 | 119 | async function generateCreateForm( 120 | modelTree: ModelTree, 121 | routeUrl: string, 122 | enums: Record<string, string[]> 123 | ): Promise<string> { 124 | const formFields = generateFormFields(modelTree, enums); 125 | const reactComponentTemplate = ` 126 | <form className="space-y-2" action={addNexquikTemplateModel}> 127 | ${formFields} 128 | <div className="flex space-x-4 pt-4"> 129 | <button type="submit" className="${blueButtonClass}" 130 | > 131 | Create NexquikTemplateModel 132 | </button> 133 | 134 | </div> 135 | </form> 136 | `; 137 | 138 | return reactComponentTemplate; 139 | } 140 | 141 | async function generateRedirect(redirectUrl: string): Promise<string> { 142 | const reactComponentTemplate = ` 143 | redirect(${redirectUrl}); 144 | `; 145 | return reactComponentTemplate; 146 | } 147 | 148 | async function generateLink( 149 | linkUrl: string, 150 | linkText: string 151 | ): Promise<string> { 152 | const linkString = linkUrl ? `\`${linkUrl}\`` : "'/'"; 153 | const reactComponentTemplate = ` 154 | 155 | 156 | <Link href={${linkString}} className="inline-flex items-center ${blueButtonClass}"> 157 | <svg className="w-3 h-3 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> 158 | <path d="M9.546.5a9.5 9.5 0 1 0 9.5 9.5 9.51 9.51 0 0 0-9.5-9.5ZM13.788 11h-3.242v3.242a1 1 0 1 1-2 0V11H5.304a1 1 0 0 1 0-2h3.242V5.758a1 1 0 0 1 2 0V9h3.242a1 1 0 1 1 0 2Z"/> 159 | </svg> 160 | ${linkText} 161 | </Link> `; 162 | return reactComponentTemplate; 163 | } 164 | 165 | async function generateRevalidatePath( 166 | revalidationUrl: string 167 | ): Promise<string> { 168 | const reactComponentTemplate = ` 169 | revalidatePath(\`${revalidationUrl}\`); 170 | `; 171 | return reactComponentTemplate; 172 | } 173 | 174 | async function generateEditForm( 175 | modelTree: ModelTree, 176 | routeUrl: string, 177 | enums: Record<string, string[]> 178 | ): Promise<string> { 179 | const formFields = generateFormFieldsWithDefaults( 180 | modelTree.model.fields, 181 | enums 182 | ); 183 | // Define the React component template as a string 184 | const reactComponentTemplate = ` 185 | <form className="space-y-2" action={editNexquikTemplateModel}> 186 | ${formFields} 187 | <div className="flex space-x-4 pt-4"> 188 | <button className="${blueButtonClass}" type="submit">Update NexquikTemplateModel</button> 189 | <Link href={\`${routeUrl}\`} className="${grayButtonClass}" passHref> 190 | Cancel 191 | </Link> 192 | </div> 193 | </form> 194 | `; 195 | 196 | return reactComponentTemplate; 197 | } 198 | ``; 199 | function getEnums(datamodel: DMMF.Datamodel): Record<string, string[]> { 200 | const enums: Record<string, string[]> = {}; 201 | 202 | for (const enumDef of datamodel.enums) { 203 | const enumName = enumDef.name; 204 | const enumValues = enumDef.values.map((value) => value.name); 205 | enums[enumName] = enumValues; 206 | } 207 | 208 | return enums; 209 | } 210 | async function generateChildrenList( 211 | modelTree: ModelTree, 212 | routeUrl: string 213 | ): Promise<string> { 214 | // Define the React component template as a string 215 | const slug = getDynamicSlugs( 216 | modelTree.model.name, 217 | modelTree.uniqueIdentifierField.map((f) => f.name) 218 | ); 219 | const childrenLinks: string[] = []; 220 | 221 | const childList = ` <table className="w-full text-left border-collapse mt-4"> 222 | 223 | <thead> 224 | 225 | <tr> 226 | <th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white p-0 dark:bg-slate-900 ${darkTextClass}"> <div className="py-2 pr-2 border-b border-slate-200 dark:border-slate-400/20">Children</div> </th> 227 | </tr> 228 | </thead> 229 | 230 | <tbody className="align-baseline"> 231 | 232 | `; 233 | modelTree.children.forEach((c) => { 234 | let childLink = ` 235 | 236 | <tr key={'${c.modelName}'}> 237 | <td translate="no" 238 | className="py-2 pr-2 leading-6 "> 239 | <Link 240 | className="${grayButtonClass}" 241 | href={\`${routeUrl}/`; 242 | 243 | slug.forEach((s) => { 244 | childLink += `\${params.${s}}/`; 245 | }); 246 | 247 | childLink += `${ 248 | c.modelName.charAt(0).toLowerCase() + c.modelName.slice(1) 249 | }\`} > 250 | ${c.modelName} 251 | </Link> 252 | </td> 253 | </tr>`; 254 | childrenLinks.push(childLink); 255 | }); 256 | return childList + childrenLinks.join("\n") + "</tbody></table>"; 257 | } 258 | async function generateListForm( 259 | modelTree: ModelTree, 260 | routeUrl: string, 261 | uniqueFields: { 262 | name: string; 263 | type: string; 264 | }[], 265 | idFields: string[] 266 | ): Promise<string> { 267 | let linkHref = routeUrl; 268 | uniqueFields.forEach((f) => { 269 | linkHref += "/" + "${" + "nexquikTemplateModel." + f.name + "}"; 270 | }); 271 | 272 | let uniqueFormInputs = ""; 273 | uniqueFields.forEach((u) => { 274 | uniqueFormInputs += `<input hidden ${ 275 | u.type === "Float" ? 'step="0.01"' : "" 276 | } type="${prismaFieldToInputType[u.type]}" name="${ 277 | u.name 278 | }" value={nexquikTemplateModel.${u.name}} readOnly/>`; 279 | }); 280 | 281 | // Define the React component template as a string 282 | const reactComponentTemplate = ` 283 | <table className="w-full text-left border-collapse mt-2 border-b border-slate-200 dark:border-slate-400/20"> 284 | <thead> 285 | 286 | <tr> 287 | ${idFields 288 | .map((field) => { 289 | return `<th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white dark:bg-slate-900 p-0 ${darkTextClass}"> <div className="py-2 pr-2 border-b border-slate-200 dark:border-slate-400/20">${field} </div> </th>`; 290 | }) 291 | .join("\n")} 292 | <th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white p-0 dark:bg-slate-900 ${darkTextClass}"> <div className="py-2 pr-2 border-b border-slate-200 dark:border-slate-400/20">Actions </div> </th> 293 | </tr> 294 | </thead> 295 | <tbody className="align-baseline"> 296 | 297 | {nexquikTemplateModel?.map((nexquikTemplateModel, index) => ( 298 | <tr key={index}> 299 | 300 | ${idFields 301 | .map((field) => { 302 | return `<td translate="no" 303 | className="py-2 pr-2 font-bold text-sm leading-6 whitespace-nowrap ${lightTextClass}"> {\`\${nexquikTemplateModel.${field}}\`} </td>`; 304 | }) 305 | .join("\n")} 306 | 307 | <td className="py-2 pr-2 font-bold text-sm leading-6 text-sky-500 whitespace-nowrap ${lightTextClass}"> 308 | <form className="flex space-x-2"> 309 | ${uniqueFormInputs} 310 | 311 | 312 | 313 | <div className="inline-flex rounded-md shadow-sm" role="group"> 314 | 315 | <Link href={\`${linkHref}\`} className="inline-flex items-center px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-l-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"> 316 | <svg className="w-3 h-3 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> 317 | <path d="M10 0C4.612 0 0 5.336 0 7c0 1.742 3.546 7 10 7 6.454 0 10-5.258 10-7 0-1.664-4.612-7-10-7Zm0 10a3 3 0 1 1 0-6 3 3 0 0 1 0 6Z"/> 318 | </svg> 319 | View 320 | </Link> 321 | 322 | 323 | <Link href={\`${linkHref}/edit\`} className="inline-flex items-center px-2 py-1 text-sm font-medium text-gray-900 bg-white border-t border-b border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"> 324 | <svg className="w-3 h-3 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"> 325 | <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m13.835 7.578-.005.007-7.137 7.137 2.139 2.138 7.143-7.142-2.14-2.14Zm-10.696 3.59 2.139 2.14 7.138-7.137.007-.005-2.141-2.141-7.143 7.143Zm1.433 4.261L2 12.852.051 18.684a1 1 0 0 0 1.265 1.264L7.147 18l-2.575-2.571Zm14.249-14.25a4.03 4.03 0 0 0-5.693 0L11.7 2.611 17.389 8.3l1.432-1.432a4.029 4.029 0 0 0 0-5.689Z"/> 326 | 327 | </svg> 328 | Edit 329 | </Link> 330 | 331 | 332 | <button formAction={deleteNexquikTemplateModel} className="inline-flex items-center px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-r-md hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-red-900 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-red-800 dark:focus:ring-blue-500 dark:focus:text-white"> 333 | <svg className="w-3 h-3 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> 334 | <path d="M17 4h-4V2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v2H1a1 1 0 0 0 0 2h1v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1a1 1 0 1 0 0-2ZM7 2h4v2H7V2Zm1 14a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v8Zm4 0a1 1 0 0 1-2 0V8a1 1 0 0 1 2 0v8Z"/> 335 | 336 | </svg> 337 | Delete 338 | </button> 339 | </div> 340 | 341 | 342 | 343 | 344 | 345 | </form> 346 | 347 | </td> 348 | </tr> 349 | 350 | ))} 351 | </tbody> 352 | </table> 353 | `; 354 | 355 | return reactComponentTemplate; 356 | } 357 | 358 | export async function generate( 359 | prismaSchemaPath: string, 360 | outputDirectory: string, 361 | maxAllowedDepth: number, 362 | init: boolean, 363 | rootName: string, 364 | groups: Group[], 365 | extendOnly: boolean, 366 | deps: boolean, 367 | prismaImportString: string, 368 | appTitle: string, 369 | modelsOnly: boolean 370 | ) { 371 | // Read the Prisma schema file 372 | const prismaSchema = await readFileAsync(prismaSchemaPath, "utf-8"); 373 | 374 | // Main section to build the app from the modelTree 375 | const dmmf = await getDMMF({ datamodel: prismaSchema }); 376 | 377 | // i.e. devNexquikApp/app 378 | const outputAppDirectory = path.join(outputDirectory, "app"); 379 | createNestedDirectory(outputAppDirectory); 380 | 381 | // i.e. devNexquikApp/app/gen 382 | const outputRouteGroup = path.join(outputAppDirectory, rootName); 383 | 384 | // Remove the directory if it exists 385 | // Check if the directory exists 386 | if (extendOnly === false) { 387 | if (fse.existsSync(outputRouteGroup)) { 388 | // Remove the directory and its contents 389 | fse.removeSync(outputRouteGroup); 390 | } 391 | } else { 392 | console.log(`Extending directory '${outputRouteGroup}'.`); 393 | } 394 | createNestedDirectory(path.join(outputAppDirectory, rootName)); 395 | 396 | const enums = getEnums(dmmf.datamodel); 397 | 398 | const imagesDirectoryName = "images"; 399 | // Create files in main directory if we are initializing a new app 400 | if (init === true) { 401 | copyDirectory(path.join(__dirname, "templateRoot"), outputDirectory, true, [ 402 | "app", 403 | "public", 404 | ]); 405 | 406 | createNestedDirectory(path.join(outputDirectory, "prisma")); 407 | 408 | // Copy over the user's prisma schema and rename it to schema.prisma 409 | copyAndRenameFile( 410 | prismaSchemaPath, 411 | path.join(outputDirectory, "prisma"), 412 | "schema.prisma" 413 | ); 414 | 415 | // Copy over tsconfig 416 | fs.copyFile( 417 | path.join(__dirname, "templateRoot", "tsconfig.json"), 418 | path.join(outputDirectory, "tsconfig.json"), 419 | (err) => { 420 | if (err) { 421 | console.error("An error occurred while copying the file:", err); 422 | } 423 | } 424 | ); 425 | createNestedDirectory(path.join(outputDirectory, "app")); 426 | 427 | // Copy over root app layout 428 | fs.copyFile( 429 | path.join(__dirname, "templateRoot", "app", "layout.tsx"), 430 | path.join(outputDirectory, "app", "layout.tsx"), 431 | (err) => { 432 | if (err) { 433 | console.error("An error occurred while copying the file:", err); 434 | } 435 | } 436 | ); 437 | 438 | fs.copyFile( 439 | path.join(__dirname, "templateRoot", "app", "page.tsx"), 440 | path.join(outputDirectory, "app", "page.tsx"), 441 | (err) => { 442 | if (err) { 443 | console.error("An error occurred while copying the file:", err); 444 | } 445 | } 446 | ); 447 | await modifyFile( 448 | path.join(__dirname, "templateRoot", "app", "page.tsx"), 449 | path.join(outputDirectory, "app", "page.tsx"), 450 | [ 451 | { 452 | startComment: "// @nexquik homeRedirect start", 453 | endComment: "// @nexquik homeRedirect stop", 454 | insertString: `redirect("/${rootName}")`, 455 | }, 456 | ] 457 | ); 458 | } else if (deps === true) { 459 | // Install deps in output directory 460 | try { 461 | console.log( 462 | `${chalk.blue.bold("Installing dependencies...")} ${chalk.gray( 463 | "(This may take a moment, and only needs to be run once in your peoject." 464 | )}` 465 | ); 466 | installPackages({ 467 | sourcePackageJson: path.join(__dirname, "templateRoot", "package.json"), 468 | destinationDirectory: outputDirectory, 469 | }); 470 | // console.log("Dependencies installed successfully."); 471 | } catch (error) { 472 | console.error("Error installing dependencies:", error); 473 | } 474 | // Copy over tailwind.config.js and postcss.config 475 | fs.copyFile( 476 | path.join(__dirname, "templateRoot", "tailwind.config.js"), 477 | path.join(outputDirectory, "tailwind.config.js"), 478 | (err) => { 479 | if (err) { 480 | console.error("An error occurred while copying the file:", err); 481 | } 482 | } 483 | ); 484 | 485 | fs.copyFile( 486 | path.join(__dirname, "templateRoot", "postcss.config.js"), 487 | path.join(outputDirectory, "postcss.config.js"), 488 | (err) => { 489 | if (err) { 490 | console.error("An error occurred while copying the file:", err); 491 | } 492 | } 493 | ); 494 | } 495 | 496 | if (groups.length > 0) { 497 | console.log( 498 | `${chalk.blue.bold( 499 | "Generating directories for your models..." 500 | )} ${chalk.gray("(For deeply-nested schemas, this may take a moment)")}` 501 | ); 502 | // Create grouped route directories 503 | groups.forEach(async ({ name, include, exclude }) => { 504 | // Create model tree and verify there is at least one valid model 505 | const modelTree = createModelTree(dmmf.datamodel, exclude, include); 506 | if (modelTree.length === 0) { 507 | console.log(chalk.red(`No valid models detected for group: ${name}`)); 508 | throw new Error(`No valid models detected for group: ${name}`); 509 | } 510 | const thisGroupPath = `${rootName && rootName + "/"}${name}`; 511 | const thisOutputRouteGroupPath = path.join( 512 | outputAppDirectory, 513 | rootName, 514 | name 515 | ); 516 | // Create base directory for this model under the app dir 517 | if (fse.existsSync(path.join(outputAppDirectory, rootName, name))) { 518 | // Remove the directory and its contents 519 | fse.removeSync(path.join(outputAppDirectory, rootName, name)); 520 | } 521 | await generateAppDirectoryFromModelTree( 522 | modelTree, 523 | outputAppDirectory, 524 | enums, 525 | maxAllowedDepth, 526 | thisGroupPath, 527 | prismaImportString 528 | ); 529 | 530 | // Nested Group Home route list 531 | const modelNames = modelTree.map((m) => m.model.name); 532 | const routeList = generateRouteList(modelNames, rootName, name); 533 | 534 | if (!modelsOnly) { 535 | createNestedDirectory( 536 | path.join(outputDirectory, "app", rootName, imagesDirectoryName) 537 | ); 538 | 539 | copyDirectory( 540 | path.join(__dirname, "templateRoot", "public"), 541 | path.join(outputDirectory, "app", rootName, imagesDirectoryName), 542 | true 543 | ); 544 | await modifyFile( 545 | path.join(__dirname, "templateRoot", "app", "groupRouteHome.tsx"), 546 | path.join(path.join(thisOutputRouteGroupPath, "page.tsx")), 547 | [ 548 | { 549 | startComment: "{/* @nexquik routeList start */}", 550 | endComment: "{/* @nexquik routeList stop */}", 551 | insertString: routeList, 552 | }, 553 | { 554 | startComment: "{/* @nexquik routeGroupName start */}", 555 | endComment: "{/* @nexquik routeGroupName start */}", 556 | insertString: name, 557 | }, 558 | ] 559 | ); 560 | // globals.css 561 | fs.copyFile( 562 | path.join(__dirname, "templateRoot", "app", "globals.css"), 563 | path.join(outputRouteGroup, "globals.css"), 564 | (err) => { 565 | if (err) { 566 | console.error("An error occurred while copying the file:", err); 567 | } 568 | } 569 | ); 570 | } 571 | }); 572 | 573 | // END GROUP LOOP 574 | 575 | const entries = fs.readdirSync(outputRouteGroup, { withFileTypes: true }); 576 | 577 | // Filter only directory entries 578 | const directories = entries 579 | .filter( 580 | (entry) => entry.isDirectory() && entry.name !== imagesDirectoryName 581 | ) 582 | .map((entry) => entry.name); 583 | 584 | // console.log("Directories in", directoryPath, ":", directories); 585 | 586 | // Route sidebar 587 | let routeSidebar = ""; 588 | for (const dir of directories) { 589 | routeSidebar += `<li className="mt-4"> 590 | 591 | <a 592 | href="/${rootName}/${dir}" 593 | className="pl-2 mb-8 lg:mb-1 font-semibold dark:text-sky-400 hover:text-sky-500 dark:hover:text-sky-600" 594 | > 595 | ${dir} 596 | </a> 597 | </li> 598 | 599 | `; 600 | if (!modelsOnly) { 601 | // layout.tsx 602 | await modifyFile( 603 | path.join( 604 | path.join( 605 | __dirname, 606 | "templateRoot", 607 | "app", 608 | "rootGroupRouteLayout.tsx" 609 | ) 610 | ), 611 | path.join(path.join(outputRouteGroup, "layout.tsx")), 612 | [ 613 | { 614 | startComment: "{/* @nexquik appTitle start */}", 615 | endComment: "{/* @nexquik appTitle stop */}", 616 | insertString: appTitle, 617 | }, 618 | { 619 | startComment: "{/* //@nexquik routeSidebar start */}", 620 | endComment: "{/* //@nexquik routeSidebar stop */}", 621 | insertString: routeSidebar, 622 | }, 623 | ] 624 | ); 625 | 626 | const routeGroupList = generateRouteGroupList(rootName, directories); 627 | 628 | await modifyFile( 629 | path.join(__dirname, "templateRoot", "app", "rootGroupRouteHome.tsx"), 630 | path.join(outputRouteGroup, "page.tsx"), 631 | [ 632 | { 633 | startComment: "{/* @nexquik routeGroupList start */}", 634 | endComment: "{/* @nexquik routeGroupList stop */}", 635 | insertString: routeGroupList, 636 | }, 637 | ] 638 | ); 639 | } 640 | } 641 | } else { 642 | console.log(`${chalk.blue.bold("No groups specified.")}`); 643 | } 644 | return; 645 | } 646 | 647 | export async function generateShowForm( 648 | modelTree: ModelTree, 649 | routeUrl: string, 650 | childModelLinkList: string 651 | ): Promise<string> { 652 | const uniqueFields = modelTree.uniqueIdentifierField; 653 | 654 | let linkRoute = routeUrl; 655 | uniqueFields.forEach((f) => { 656 | linkRoute += `\${nexquikTemplateModel?.${f?.name}}/`; 657 | }); 658 | const reactComponentTemplate = ` 659 | 660 | <form className="space-y-2"> 661 | ${modelTree.model.fields 662 | .map((field) => { 663 | if (!isFieldRenderable(field)) { 664 | return ""; 665 | } 666 | let typecastValue = `nexquikTemplateModel?.${field?.name}`; 667 | if (field?.type === "Int" || field?.type === "Float") { 668 | typecastValue = `Number(${typecastValue})`; 669 | } else { 670 | typecastValue = `String(${typecastValue})`; 671 | } 672 | return ` <input hidden type="${field.type}" name="${field?.name}" ${ 673 | field.type === "Float" ? 'step="0.01"' : "" 674 | } defaultValue={${typecastValue}} />`; 675 | }) 676 | .join("\n")} 677 | 678 | 679 | 680 | 681 | 682 | <table className="w-full text-left border-collapse"> 683 | <thead> 684 | <tr> 685 | 686 | <th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white p-0 dark:bg-slate-900 ${darkTextClass}"> <div className="py-2 pr-2 border-b border-slate-200 dark:border-slate-400/20">Field </div> </th> 687 | <th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white p-0 dark:bg-slate-900 ${darkTextClass}"> <div className="py-2 pr-2 border-b border-slate-200 dark:border-slate-400/20">Value </div> </th> 688 | 689 | </tr> 690 | </thead> 691 | 692 | 693 | 694 | <tbody className="align-baseline"> 695 | 696 | ${modelTree.model.fields 697 | .map((field) => { 698 | if (!isFieldRenderable(field)) { 699 | return ""; 700 | } 701 | return ` 702 | 703 | <tr key={'${field.name}'}> 704 | <td translate="no" 705 | className="py-2 pr-2 font-bold text-sm leading-6 whitespace-nowrap ${lightTextClass}"> ${field.name} </td> 706 | <td translate="no" 707 | className="py-2 pr-2 font-medium text-sm leading-6 text-slate-700 whitespace-nowrap dark:text-slate-400"> {\`\${nexquikTemplateModel?.${field.name}}\`} </td> 708 | 709 | 710 | </tr> 711 | 712 | 713 | 714 | 715 | `; 716 | }) 717 | .join("\n")} 718 | 719 | 720 | </tbody> 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | </table> 729 | 730 | 731 | <div className="inline-flex rounded-md shadow-sm" role="group"> 732 | 733 | <Link href={\`${linkRoute}/edit\`} className="inline-flex items-center px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-l-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"> 734 | <svg className="w-3 h-3 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20"> 735 | <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m13.835 7.578-.005.007-7.137 7.137 2.139 2.138 7.143-7.142-2.14-2.14Zm-10.696 3.59 2.139 2.14 7.138-7.137.007-.005-2.141-2.141-7.143 7.143Zm1.433 4.261L2 12.852.051 18.684a1 1 0 0 0 1.265 1.264L7.147 18l-2.575-2.571Zm14.249-14.25a4.03 4.03 0 0 0-5.693 0L11.7 2.611 17.389 8.3l1.432-1.432a4.029 4.029 0 0 0 0-5.689Z"/> 736 | 737 | </svg> 738 | Edit 739 | </Link> 740 | 741 | 742 | 743 | <button formAction={deleteNexquikTemplateModel} className="inline-flex items-center px-2 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-r-md hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-red-900 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-red-800 dark:focus:ring-blue-500 dark:focus:text-white"> 744 | <svg className="w-3 h-3 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> 745 | <path d="M17 4h-4V2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v2H1a1 1 0 0 0 0 2h1v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1a1 1 0 1 0 0-2ZM7 2h4v2H7V2Zm1 14a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v8Zm4 0a1 1 0 0 1-2 0V8a1 1 0 0 1 2 0v8Z"/> 746 | 747 | </svg> 748 | Delete 749 | </button> 750 | </div> 751 | 752 | 753 | </form> 754 | ${childModelLinkList} 755 | 756 | `; 757 | 758 | return reactComponentTemplate; 759 | } 760 | 761 | function generateRouteGroupList(rootName: string, dirs: string[]) { 762 | const routeLinks = []; 763 | for (const name of dirs) { 764 | // const lowerCase = model.charAt(0).toLowerCase() + model.slice(1); 765 | routeLinks.push(`<tr> 766 | 767 | <td 768 | translate="no" 769 | className="py-2 pr-2 font-medium text-sm leading-6 whitespace-nowrap " 770 | > 771 | 772 | 773 | <a className=" mb-8 lg:mb-1 font-semibold dark:text-sky-400 hover:text-sky-500 dark:hover:text-sky-600" 774 | href="/${rootName}/${name}"> ${name} 775 | </a> 776 | </td> 777 | 778 | </tr> 779 | `); 780 | } 781 | 782 | return ` 783 | 784 | <table className="min-w-full text-left border-collapse "> 785 | <thead> 786 | <tr> 787 | <th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white p-0 dark:bg-slate-900 ${darkTextClass}"> 788 | <div className="py-2 border-b border-slate-200 dark:border-slate-400/20"> 789 | Group 790 | </div> 791 | </th> 792 | </tr> 793 | </thead> 794 | 795 | <tbody className="align-baseline"> 796 | ${routeLinks.join("\n")} 797 | </tbody> 798 | </table> 799 | 800 | `; 801 | } 802 | 803 | function generateRouteList( 804 | modelNames: string[], 805 | routeGroup: string, 806 | groupName: string 807 | ) { 808 | const routeLinks = []; 809 | for (const model of modelNames) { 810 | const lowerCase = model.charAt(0).toLowerCase() + model.slice(1); 811 | routeLinks.push(`<tr> 812 | <td 813 | translate="no" 814 | className="py-2 pr-2 font-bold text-sm leading-6 whitespace-nowrap ${lightTextClass}" 815 | > 816 | ${model} 817 | </td> 818 | 819 | <td 820 | translate="no" 821 | className="py-2 pr-2 font-medium text-sm leading-6 whitespace-nowrap " 822 | > 823 | 824 | <a className="${blueTextClass}" href="/${routeGroup}/${groupName}/${lowerCase}/create"> 825 | Create 826 | </a> 827 | <a className="${darkTextClass}"> 828 | {' '} / {' '} 829 | </a> 830 | <a className="${blueTextClass}" href="/${routeGroup}/${groupName}/${lowerCase}"> List 831 | </a> 832 | </td> 833 | 834 | </tr> 835 | `); 836 | } 837 | 838 | return ` 839 | 840 | <table className="min-w-full text-left border-collapse "> 841 | <thead> 842 | <tr> 843 | <th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white p-0 dark:bg-slate-900 ${darkTextClass}"> 844 | <div className="py-2 border-b border-slate-200 dark:border-slate-400/20"> 845 | Model 846 | </div> 847 | </th> 848 | <th className="sticky z-10 top-0 text-sm leading-6 font-semibold bg-white p-0 dark:bg-slate-900 sm:table-cell ${darkTextClass}"> 849 | <div className="py-2 border-b border-slate-200 dark:border-slate-400/20 pr-2"> 850 | Operations 851 | </div> 852 | </th> 853 | 854 | </tr> 855 | </thead> 856 | 857 | <tbody className="align-baseline"> 858 | ${routeLinks.join("\n")} 859 | </tbody> 860 | </table> 861 | 862 | `; 863 | } 864 | 865 | export async function generateAppDirectoryFromModelTree( 866 | modelTreeArray: ModelTree[], 867 | outputDirectory: string, 868 | enums: Record<string, string[]>, 869 | maxAllowedDepth: number, 870 | rootName: string, 871 | prismaImportString: string 872 | ): Promise<RouteObject[]> { 873 | const routes: RouteObject[] = []; 874 | let fileCount = 0; 875 | let directoryCount = 0; 876 | let maxDepth = 0; 877 | async function generateRoutes( 878 | modelTree: ModelTree, 879 | parentRoute: { 880 | name: string; 881 | uniqueIdentifierField: { name: string; type: string }[]; 882 | }, 883 | depth = 0, 884 | maxAllowedDepth: number, 885 | firstPass = false 886 | ) { 887 | if (depth > maxAllowedDepth) { 888 | maxDepth = maxAllowedDepth; 889 | process.stdout.write( 890 | chalk.yellow(`\u001b[2K\rMax depth hit for ${modelTree.modelName}`) 891 | ); 892 | return; 893 | } 894 | if (depth > maxDepth) { 895 | maxDepth = depth; 896 | } 897 | process.stdout.write(`\u001b[2K\r${modelTree.model.name} - Depth ${depth}`); 898 | process.stdout.write(`\u001b[2K\r`); 899 | 900 | let childLoopPromises: Promise<void>[] = []; 901 | directoryCount += 3; 902 | fileCount += 4; 903 | // Get the current model name 904 | const modelName = modelTree.modelName; 905 | 906 | // Get the current mode'ls array of prisma unique id fields 907 | const modelUniqueIdentifierField = modelTree.uniqueIdentifierField; 908 | 909 | let route = parentRoute.name; 910 | 911 | route += modelName.charAt(0).toLowerCase() + modelName.slice(1) + "/"; 912 | 913 | routes.push({ 914 | segment: `${route}create`, 915 | model: modelName, 916 | operation: "Create", 917 | description: `Create a ${modelName}`, 918 | }); 919 | 920 | // List 921 | routes.push({ 922 | segment: route, 923 | model: modelName, 924 | operation: "List", 925 | description: `Get a list of ${modelName}s`, 926 | }); 927 | let splitRoute = ""; 928 | const slugss = getDynamicSlugs( 929 | modelName, 930 | modelTree.uniqueIdentifierField.map((f) => f.name) 931 | ); 932 | slugss.forEach((s) => { 933 | splitRoute += `[${s}]`; 934 | }); 935 | 936 | routes.push({ 937 | segment: `${route}${splitRoute}/edit`, 938 | model: modelName, 939 | operation: "Edit", 940 | description: `Edit a ${modelName} by ID`, 941 | }); 942 | 943 | // Show 944 | routes.push({ 945 | segment: `${route}${splitRoute}`, 946 | model: modelName, 947 | operation: "Show", 948 | description: `Get details of a ${modelName} by ID`, 949 | }); 950 | 951 | const baseRoute = route; 952 | const createRedirectForm = convertRouteToRedirectUrl(baseRoute); 953 | 954 | const baseModelDirectory = path.join(outputDirectory, route); 955 | createNestedDirectory(baseModelDirectory); 956 | 957 | // Create create directory 958 | createNestedDirectory(path.join(baseModelDirectory, "create")); 959 | 960 | const templateModelDirectory = path.join( 961 | __dirname, 962 | "templateRoot", 963 | "app", 964 | "nexquikTemplateModel" 965 | ); 966 | const createBreadCrumb = generateBreadCrumb(route + "/create"); 967 | 968 | const listBreadCrumb = generateBreadCrumb(route); 969 | 970 | // Create dynamic directories 971 | const slugsForThisModel = getDynamicSlugs( 972 | modelTree.modelName, 973 | modelUniqueIdentifierField.map((f) => f.name) 974 | ); 975 | slugsForThisModel.forEach((parentSlug) => { 976 | route += `[${parentSlug}]/`; 977 | }); 978 | 979 | // Create dynamic and edit directory 980 | const dynamicOutputDirectory = path.join(outputDirectory, route); 981 | createNestedDirectory(dynamicOutputDirectory); 982 | createNestedDirectory(path.join(dynamicOutputDirectory, "edit")); 983 | 984 | const templateDynamicDirectory = path.join( 985 | __dirname, 986 | "templateRoot", 987 | "app", 988 | "nexquikTemplateModel", 989 | "[id]" 990 | ); 991 | 992 | // ############### List Page 993 | const idFields = modelTree.uniqueIdentifierField; 994 | let select = ""; 995 | if (idFields.length > 0) { 996 | select += "select:{"; 997 | idFields.forEach(({ name }, index) => { 998 | if (index > 0) { 999 | select += ","; 1000 | } 1001 | select += `${name}: true`; 1002 | }); 1003 | select += "}, "; 1004 | } 1005 | select += ` skip: (page - 1) * limit, 1006 | take: limit`; 1007 | const listFormCode = await generateListForm( 1008 | modelTree, 1009 | createRedirectForm, 1010 | modelUniqueIdentifierField, 1011 | idFields.map((f) => f.name) 1012 | ); 1013 | 1014 | // Get relation fields to parent 1015 | let relationFieldToParent = ""; 1016 | let fieldType = ""; 1017 | if (modelTree.parent) { 1018 | // Get the field on the current model that is the id referencing the parent 1019 | modelTree.model.fields.forEach((mf) => { 1020 | if (mf.type === modelTree.parent?.name) { 1021 | if ( 1022 | mf.relationFromFields?.length && 1023 | mf.relationFromFields.length > 0 1024 | ) { 1025 | relationFieldToParent = mf.relationFromFields[0]; 1026 | } 1027 | } 1028 | }); 1029 | 1030 | // Get the field type on the current model that is the id referencing the parent 1031 | fieldType = 1032 | modelTree.model.fields.find((f) => f.name === relationFieldToParent) 1033 | ?.type || ""; 1034 | } 1035 | const parentIdentifierFields = modelTree.model.fields.find((field) => 1036 | field.relationFromFields?.includes(relationFieldToParent) 1037 | )?.relationToFields; 1038 | let parentIdentifierField = ""; 1039 | if (parentIdentifierFields && parentIdentifierFields.length > 0) { 1040 | parentIdentifierField = parentIdentifierFields[0]; 1041 | } 1042 | // Delete Where Clause 1043 | const deleteWhereClause = generateDeleteClause(modelUniqueIdentifierField); 1044 | 1045 | const listRedirect = await generateRedirect(`\`${createRedirectForm}\``); 1046 | 1047 | // In parent, loop through fields and find field of 'type' current model 1048 | let isManyToMany = false; 1049 | let referenceFieldNameToParent = ""; 1050 | const parentIdField = modelTree.parent?.fields.find((f) => f.isId); 1051 | const relationNameToParent = modelTree.parent?.fields.find( 1052 | (a) => a.type === modelTree.modelName 1053 | )?.relationName; 1054 | if (relationNameToParent) { 1055 | const referenceFieldToParent = modelTree.model.fields.find( 1056 | (f) => f.relationName === relationNameToParent 1057 | ); 1058 | if (referenceFieldToParent) { 1059 | referenceFieldNameToParent = referenceFieldToParent.name; 1060 | } 1061 | if (referenceFieldToParent?.isList) { 1062 | isManyToMany = true; 1063 | } 1064 | } 1065 | let manyToManyWhere = ""; 1066 | let manyToManyConnect = ""; 1067 | if (isManyToMany && parentIdField) { 1068 | let typecastValue = `params.${getDynamicSlugs(modelTree.parent?.name, [ 1069 | parentIdField.name, 1070 | ])}`; 1071 | if (parentIdField?.type === "Int" || parentIdField?.type === "Float") { 1072 | typecastValue = `Number(${typecastValue})`; 1073 | } else if (parentIdField?.type === "Boolean") { 1074 | typecastValue = `Boolean(${typecastValue})`; 1075 | } else if (parentIdField?.type === "String") { 1076 | typecastValue = `String(${typecastValue})`; 1077 | } 1078 | manyToManyWhere = ` 1079 | where: { 1080 | ${referenceFieldNameToParent}: { 1081 | some: { 1082 | ${parentIdField?.name}: { 1083 | equals: ${typecastValue} 1084 | } 1085 | } 1086 | } 1087 | } 1088 | `; 1089 | 1090 | manyToManyConnect = ` 1091 | ${referenceFieldNameToParent}: { 1092 | connect: { 1093 | ${parentIdField?.name}: ${typecastValue}, 1094 | }, 1095 | }, 1096 | `; 1097 | } 1098 | 1099 | // Unique field on parent, that points to this model 1100 | const parentReferenceField = getParentReferenceField(modelTree); 1101 | const relationsToParent = modelTree.model.fields.find( 1102 | (f) => f.name === parentReferenceField 1103 | )?.relationToFields; 1104 | 1105 | const parentReferenceFieldType = modelTree.parent?.fields.find( 1106 | (f) => relationsToParent && relationsToParent[0] === f.name 1107 | )?.type; 1108 | 1109 | const whereparentClause = modelTree.parent 1110 | ? generateWhereParentClause( 1111 | "params", 1112 | getDynamicSlugs(modelTree.parent.name, [parentIdentifierField])[0], 1113 | relationsToParent ? relationsToParent[0] : parentIdentifierField, 1114 | parentReferenceFieldType || fieldType, 1115 | getParentReferenceField(modelTree), 1116 | manyToManyWhere, 1117 | select 1118 | ) 1119 | : `({${select}})`; 1120 | // Enum import for create and edit pages 1121 | const enumImport = Object.keys(enums) 1122 | .map((e) => `import { ${e} } from "@prisma/client";`) 1123 | .join("\n"); 1124 | 1125 | const linkHref = createRedirectForm; 1126 | 1127 | const listPagination = ` 1128 | 1129 | 1130 | <div className="flex flex-col mt-2"> 1131 | <span className="text-sm text-gray-700 dark:text-gray-400"> 1132 | Showing <span className="font-semibold text-gray-900 dark:text-white">{(page - 1) * limit + 1}</span> to <span className="font-semibold text-gray-900 dark:text-white">{Math.min(page * limit, count)}</span> of <span className="font-semibold text-gray-900 dark:text-white">{count}</span> Entries 1133 | </span> 1134 | <div className="inline-flex mt-2 xs:mt-0"> 1135 | <Link href={{ 1136 | pathname: \`${linkHref}\`, 1137 | query: { 1138 | page: page != 1 ? page - 1 : 1, 1139 | }, 1140 | }} className="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 rounded-l hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"> 1141 | Prev 1142 | </Link> 1143 | <Link href={{ 1144 | pathname: \`${linkHref}\`, 1145 | query: { 1146 | page: page >= Math.ceil(count / limit) ? page : page + 1, 1147 | }, 1148 | }} className="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 border-0 border-l border-gray-700 rounded-r hover:bg-gray-900 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"> 1149 | Next 1150 | </Link> 1151 | </div> 1152 | </div> 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | `; 1159 | 1160 | const countWhereparentClause = modelTree.parent 1161 | ? generateWhereParentClause( 1162 | "params", 1163 | getDynamicSlugs(modelTree.parent.name, [parentIdentifierField])[0], 1164 | relationsToParent ? relationsToParent[0] : parentIdentifierField, 1165 | parentReferenceFieldType || fieldType, 1166 | getParentReferenceField(modelTree), 1167 | manyToManyWhere, 1168 | "" 1169 | ) 1170 | : ``; 1171 | const props = modelTree.parent 1172 | ? `{ 1173 | params, 1174 | }: { 1175 | params: { [key: string]: string | string[] | undefined }; 1176 | }` 1177 | : ""; 1178 | 1179 | const listProps = modelTree.parent 1180 | ? ` { 1181 | params, 1182 | searchParams, 1183 | }: { 1184 | params: { [key: string]: string | string[] | undefined }; 1185 | searchParams?: { [key: string]: string | string[] | undefined }; 1186 | }` 1187 | : ` { 1188 | searchParams 1189 | }: { 1190 | searchParams?: { [key: string]: string | string[] | undefined }; 1191 | }`; 1192 | 1193 | const listCount = ` 1194 | const page = 1195 | typeof searchParams?.page === "string" ? Number(searchParams?.page) : 1; 1196 | const limit = 1197 | typeof searchParams?.limit === "string" ? Number(searchParams?.limit) : 10; 1198 | const count = await prisma.${ 1199 | modelName.charAt(0).toLowerCase() + modelName.slice(1) 1200 | }.count${countWhereparentClause ? countWhereparentClause : "()"}; 1201 | `; 1202 | 1203 | const uniqueDynamicSlugs = getDynamicSlugs( 1204 | modelTree.modelName, 1205 | modelUniqueIdentifierField.map((f) => f.name) 1206 | ); 1207 | 1208 | // ############### Show Page 1209 | const childModelLinkList = await generateChildrenList( 1210 | modelTree, 1211 | createRedirectForm 1212 | ); 1213 | const showFormCode = await generateShowForm( 1214 | modelTree, 1215 | createRedirectForm, 1216 | childModelLinkList 1217 | ); 1218 | 1219 | // If many to many, must do a connect 1220 | const createFormCode = await generateCreateForm( 1221 | modelTree, 1222 | createRedirectForm, 1223 | enums 1224 | ); 1225 | 1226 | let redirectStr = ""; 1227 | modelTree.uniqueIdentifierField.forEach( 1228 | (f) => (redirectStr += "/" + `\${created.${f.name}}`) 1229 | ); 1230 | const createRedirect = await generateRedirect( 1231 | `\`${createRedirectForm}${redirectStr}\`` 1232 | ); 1233 | 1234 | const createLink = await generateLink( 1235 | `${createRedirectForm}/create`, 1236 | "Create New NexquikTemplateModel" 1237 | ); 1238 | 1239 | const prismaInput = generateConvertToPrismaInputCode(modelTree); 1240 | 1241 | const parentSlugs = getDynamicSlugs( 1242 | modelTree.parent?.name, 1243 | parentRoute.uniqueIdentifierField.map((f) => f.name) 1244 | ); 1245 | 1246 | const prismaCreateInput = generateConvertToPrismaCreateInputCode( 1247 | modelTree, 1248 | parentSlugs, 1249 | manyToManyConnect 1250 | ); 1251 | 1252 | const whereClause = generateWhereClause( 1253 | "params", 1254 | uniqueDynamicSlugs, 1255 | modelUniqueIdentifierField 1256 | ); 1257 | 1258 | // ############### Edit Page 1259 | const editFormCode = await generateEditForm( 1260 | modelTree, 1261 | createRedirectForm, 1262 | enums 1263 | ); 1264 | 1265 | let redirectStr2 = ""; 1266 | uniqueDynamicSlugs.forEach( 1267 | (f) => (redirectStr2 += "/" + `\${params.${f}}`) 1268 | ); 1269 | 1270 | const editRedirect = await generateRedirect( 1271 | `\`${createRedirectForm}/${redirectStr2}\`` 1272 | ); 1273 | 1274 | // ############### Extras 1275 | const revalidatePath = await generateRevalidatePath( 1276 | `${createRedirectForm}` 1277 | ); 1278 | 1279 | const baseBreadCrumb = generateBreadCrumb(route); 1280 | 1281 | const editBreadCrumb = generateBreadCrumb(route + "/edit"); 1282 | 1283 | // dynamic/edit/page.tsx 1284 | childLoopPromises.push( 1285 | modifyFile( 1286 | path.join(templateDynamicDirectory, "edit", "page.tsx"), 1287 | path.join(dynamicOutputDirectory, "edit", "page.tsx"), 1288 | [ 1289 | { 1290 | startComment: "//@nexquik prismaImport start", 1291 | endComment: "//@nexquik prismaImport stop", 1292 | insertString: prismaImportString, 1293 | }, 1294 | { 1295 | startComment: "//@nexquik prismaEnumImport start", 1296 | endComment: "//@nexquik prismaEnumImport stop", 1297 | insertString: enumImport, 1298 | }, 1299 | { 1300 | startComment: "//@nexquik prismaWhereInput start", 1301 | endComment: "//@nexquik prismaWhereInput stop", 1302 | insertString: whereClause, 1303 | }, 1304 | { 1305 | startComment: "//@nexquik prismaEditDataInput start", 1306 | endComment: "//@nexquik prismaEditDataInput stop", 1307 | insertString: prismaInput, 1308 | }, 1309 | { 1310 | startComment: "//@nexquik editRedirect start", 1311 | endComment: "//@nexquik editRedirect stop", 1312 | insertString: editRedirect, 1313 | }, 1314 | { 1315 | startComment: "{/* @nexquik editBreadCrumb start */}", 1316 | endComment: "{/* @nexquik editBreadCrumb stop */}", 1317 | insertString: editBreadCrumb, 1318 | }, 1319 | { 1320 | startComment: "{/* @nexquik editForm start */}", 1321 | endComment: "{/* @nexquik editForm stop */}", 1322 | insertString: editFormCode, 1323 | }, 1324 | ], 1325 | modelTree.modelName 1326 | ) 1327 | ); 1328 | 1329 | // dynamic/page.tsx 1330 | childLoopPromises.push( 1331 | modifyFile( 1332 | path.join(templateDynamicDirectory, "page.tsx"), 1333 | path.join(dynamicOutputDirectory, "page.tsx"), 1334 | [ 1335 | { 1336 | startComment: "//@nexquik prismaImport start", 1337 | endComment: "//@nexquik prismaImport stop", 1338 | insertString: prismaImportString, 1339 | }, 1340 | { 1341 | startComment: "//@nexquik prismaWhereInput start", 1342 | endComment: "//@nexquik prismaWhereInput stop", 1343 | insertString: whereClause, 1344 | }, 1345 | { 1346 | startComment: "//@nexquik prismaDeleteClause start", 1347 | endComment: "//@nexquik prismaDeleteClause stop", 1348 | insertString: deleteWhereClause, 1349 | }, 1350 | { 1351 | startComment: "//@nexquik revalidatePath start", 1352 | endComment: "//@nexquik revalidatePath stop", 1353 | insertString: revalidatePath, 1354 | }, 1355 | { 1356 | startComment: "//@nexquik listRedirect start", 1357 | endComment: "//@nexquik listRedirect stop", 1358 | insertString: listRedirect, 1359 | }, 1360 | { 1361 | startComment: "{/* @nexquik breadcrumb start */}", 1362 | endComment: "{/* @nexquik breadcrumb stop */}", 1363 | insertString: baseBreadCrumb, 1364 | }, 1365 | { 1366 | startComment: "{/* @nexquik showForm start */}", 1367 | endComment: "{/* @nexquik showForm stop */}", 1368 | insertString: showFormCode, 1369 | }, 1370 | ], 1371 | modelTree.modelName 1372 | ) 1373 | ); 1374 | 1375 | // base/page.tsx 1376 | childLoopPromises.push( 1377 | modifyFile( 1378 | path.join(templateModelDirectory, "page.tsx"), 1379 | path.join(baseModelDirectory, "page.tsx"), 1380 | [ 1381 | { 1382 | startComment: "//@nexquik prismaImport start", 1383 | endComment: "//@nexquik prismaImport stop", 1384 | insertString: prismaImportString, 1385 | }, 1386 | { 1387 | startComment: "//@nexquik listProps start", 1388 | endComment: "//@nexquik listProps stop", 1389 | insertString: listProps, 1390 | }, 1391 | { 1392 | startComment: "/* @nexquik listCount start */", 1393 | endComment: "/* @nexquik listCount stop */", 1394 | insertString: listCount, 1395 | }, 1396 | { 1397 | startComment: "//@nexquik prismaWhereParentClause start", 1398 | endComment: "//@nexquik prismaWhereParentClause stop", 1399 | insertString: whereparentClause, 1400 | }, 1401 | { 1402 | startComment: "//@nexquik prismaDeleteClause start", 1403 | endComment: "//@nexquik prismaDeleteClause stop", 1404 | insertString: deleteWhereClause, 1405 | }, 1406 | { 1407 | startComment: "//@nexquik revalidatePath start", 1408 | endComment: "//@nexquik revalidatePath stop", 1409 | insertString: revalidatePath, 1410 | }, 1411 | { 1412 | startComment: "{/* @nexquik listBreadcrumb start */}", 1413 | endComment: "{/* @nexquik listBreadcrumb stop */}", 1414 | insertString: listBreadCrumb, 1415 | }, 1416 | { 1417 | startComment: "{/* @nexquik createLink start */}", 1418 | endComment: "{/* @nexquik createLink stop */}", 1419 | insertString: createLink, 1420 | }, 1421 | { 1422 | startComment: "{/* @nexquik listForm start */}", 1423 | endComment: "{/* @nexquik listForm stop */}", 1424 | insertString: listFormCode, 1425 | }, 1426 | { 1427 | startComment: "{/* @nexquik listPagination start */}", 1428 | endComment: "{/* @nexquik listPagination stop */}", 1429 | insertString: listPagination, 1430 | }, 1431 | ], 1432 | modelTree.modelName 1433 | ) 1434 | ); 1435 | 1436 | // base/create/page.tsx 1437 | childLoopPromises.push( 1438 | modifyFile( 1439 | path.join(templateModelDirectory, "create", "page.tsx"), 1440 | path.join(baseModelDirectory, "create", "page.tsx"), 1441 | [ 1442 | { 1443 | startComment: "//@nexquik prismaImport start", 1444 | endComment: "//@nexquik prismaImport stop", 1445 | insertString: prismaImportString, 1446 | }, 1447 | { 1448 | startComment: "//@nexquik props start", 1449 | endComment: "//@nexquik props stop", 1450 | insertString: props, 1451 | }, 1452 | { 1453 | startComment: "//@nexquik prismaEnumImport start", 1454 | endComment: "//@nexquik prismaEnumImport stop", 1455 | insertString: enumImport, 1456 | }, 1457 | { 1458 | startComment: "//@nexquik prismaCreateDataInput start", 1459 | endComment: "//@nexquik prismaCreateDataInput stop", 1460 | insertString: prismaCreateInput, 1461 | }, 1462 | { 1463 | startComment: "//@nexquik revalidatePath start", 1464 | endComment: "//@nexquik revalidatePath stop", 1465 | insertString: revalidatePath, 1466 | }, 1467 | { 1468 | startComment: "//@nexquik createRedirect start", 1469 | endComment: "//@nexquik createRedirect stop", 1470 | insertString: createRedirect, 1471 | }, 1472 | 1473 | { 1474 | startComment: "{/* @nexquik createBreadcrumb start */}", 1475 | endComment: "{/* @nexquik createBreadcrumb stop */}", 1476 | insertString: createBreadCrumb, 1477 | }, 1478 | { 1479 | startComment: "{/* @nexquik createForm start */}", 1480 | endComment: "{/* @nexquik createForm stop */}", 1481 | insertString: createFormCode, 1482 | }, 1483 | ], 1484 | modelTree.modelName 1485 | ) 1486 | ); 1487 | 1488 | childLoopPromises = childLoopPromises.concat( 1489 | modelTree.children.map(async (child) => { 1490 | try { 1491 | await generateRoutes( 1492 | child, 1493 | { 1494 | name: route, 1495 | uniqueIdentifierField: modelUniqueIdentifierField, 1496 | }, 1497 | depth + 1, 1498 | maxAllowedDepth 1499 | ); 1500 | } catch (error) { 1501 | console.error("An error occurred in childLoopPromises:", error); 1502 | } 1503 | }) 1504 | ); 1505 | await Promise.all(childLoopPromises); 1506 | } 1507 | 1508 | const startTime = new Date().getTime(); 1509 | 1510 | // Start the main loop 1511 | const mainLoopPromises = modelTreeArray.map(async (modelTree) => { 1512 | try { 1513 | await generateRoutes( 1514 | modelTree, 1515 | { 1516 | name: `/${rootName}/`, 1517 | uniqueIdentifierField: [], 1518 | }, 1519 | 0, 1520 | maxAllowedDepth, 1521 | true 1522 | ); 1523 | } catch (error) { 1524 | console.error("An error occurred in mainLoopPromises:", error); 1525 | } 1526 | }); 1527 | 1528 | // Wait for all loop promises to complete 1529 | await Promise.all(mainLoopPromises); 1530 | process.stdout.write(`\u001b[2K\r`); 1531 | const endTime = new Date().getTime(); 1532 | const duration = (endTime - startTime) / 1000; 1533 | console.log( 1534 | `${chalk.blue.bold( 1535 | `\nCreated ${modelTreeArray.length} model(s) in Group: ` 1536 | )}${chalk.yellow.bold(`'${rootName}'`)} ${chalk.gray( 1537 | `(Generated ${fileCount} files and ${directoryCount} directories in ${duration} seconds)` 1538 | )}` 1539 | ); 1540 | 1541 | return routes; 1542 | } 1543 | 1544 | export function generateConvertToPrismaCreateInputCode( 1545 | modelTree: ModelTree, 1546 | parentSlugs: string[], 1547 | manyToManyConnect: string 1548 | ): string { 1549 | // If model has a parent, get the parent accessor 1550 | let relationFieldsToParent: string[] = []; 1551 | let fieldType = ""; 1552 | if (modelTree.parent) { 1553 | // Get the field on the current model that is the id referencing the parent 1554 | modelTree.model.fields.forEach((mf) => { 1555 | if (mf.type === modelTree.parent?.name) { 1556 | if (mf.relationFromFields?.length && mf.relationFromFields.length > 0) { 1557 | relationFieldsToParent = mf.relationFromFields; 1558 | } 1559 | } 1560 | }); 1561 | 1562 | // Get the field type on the current model that is the id referencing the parent 1563 | fieldType = 1564 | modelTree.model.fields.find((f) => 1565 | relationFieldsToParent.find((a) => a === f.name) 1566 | )?.type || ""; 1567 | } 1568 | 1569 | const fieldsToConvert: Partial<DMMF.Field>[] = modelTree.model.fields.filter( 1570 | (field) => isFieldRenderable(field) 1571 | ); 1572 | 1573 | const convertToPrismaInputLines = fieldsToConvert.map( 1574 | ({ name, type, kind, isRequired, hasDefaultValue }) => { 1575 | const nonTypeCastedValue = `formData.get('${name}')`; 1576 | let typecastValue = nonTypeCastedValue; 1577 | if (kind === "enum") { 1578 | typecastValue += ` as ${type}`; 1579 | } else { 1580 | if (type === "Int" || type === "Float") { 1581 | typecastValue = `Number(${typecastValue})`; 1582 | } else if (type === "Boolean") { 1583 | typecastValue = `Boolean(${typecastValue})`; 1584 | } else if (type === "DateTime") { 1585 | typecastValue = `new Date(String(${typecastValue}))`; 1586 | } else { 1587 | typecastValue = `String(${typecastValue})`; 1588 | } 1589 | } 1590 | if (hasDefaultValue || !isRequired) { 1591 | return ` ${name}: ${nonTypeCastedValue} ? ${typecastValue} : undefined,`; 1592 | } 1593 | return ` ${name}: ${typecastValue},`; 1594 | } 1595 | ); 1596 | 1597 | // Convert the parent accessor differently 1598 | if (relationFieldsToParent && fieldType && modelTree.parent?.name) { 1599 | relationFieldsToParent.forEach((s, index) => { 1600 | let typecastValue = `params.${parentSlugs[index]}`; 1601 | if (fieldType === "Int" || fieldType === "Float") { 1602 | typecastValue = `Number(${typecastValue})`; 1603 | } else if (fieldType === "Boolean") { 1604 | typecastValue = `Boolean(${typecastValue})`; 1605 | } else if (fieldType === "DateTime") { 1606 | typecastValue = `new Date(String(${typecastValue}))`; 1607 | } else { 1608 | typecastValue = `String(${typecastValue})`; 1609 | } 1610 | convertToPrismaInputLines.push(` ${s}: ${typecastValue},`); 1611 | }); 1612 | } 1613 | 1614 | // Convert fields pointing to other relations differently 1615 | modelTree.model.fields.map((field) => { 1616 | if (field.kind === "object") { 1617 | const relationFrom = field.relationFromFields && field.relationFromFields; 1618 | const referencedModelName = field.type; 1619 | 1620 | if (referencedModelName === modelTree.parent?.name) { 1621 | } else if (relationFrom) { 1622 | relationFrom.forEach((rf) => { 1623 | const fieldType2 = modelTree.model.fields.find( 1624 | (f) => f.name === rf 1625 | )?.type; 1626 | 1627 | let typecastValue = `formData.get('${relationFrom}')`; 1628 | if (fieldType2 === "Int" || fieldType2 === "Float") { 1629 | typecastValue = `Number(${typecastValue})`; 1630 | } else if (fieldType2 === "Boolean") { 1631 | typecastValue = `Boolean(${typecastValue})`; 1632 | } else if (fieldType2 === "DateTime") { 1633 | typecastValue = `new Date(String(${typecastValue}))`; 1634 | } else { 1635 | typecastValue = `String(${typecastValue})`; 1636 | } 1637 | 1638 | convertToPrismaInputLines.push( 1639 | ` ${relationFrom}: ${typecastValue},` 1640 | ); 1641 | }); 1642 | } 1643 | } 1644 | }); 1645 | convertToPrismaInputLines.push(manyToManyConnect); 1646 | return `{ 1647 | ${convertToPrismaInputLines.join("\n")} 1648 | }`; 1649 | } 1650 | 1651 | export function generateConvertToPrismaInputCode(modelTree: ModelTree): string { 1652 | const fieldsToConvert: Partial<DMMF.Field>[] = modelTree.model.fields 1653 | .filter(({ isId }) => !isId) 1654 | .filter((field) => isFieldRenderable(field)); 1655 | 1656 | const convertToPrismaInputLines = fieldsToConvert.map( 1657 | ({ name, type, kind }) => { 1658 | const nonTypecastValue = `formData.get('${name}')`; 1659 | let typecastValue = nonTypecastValue; 1660 | if (kind === "enum") { 1661 | typecastValue += ` as ${type}`; 1662 | } else { 1663 | if (type === "Int" || type === "Float") { 1664 | typecastValue = `Number(${typecastValue})`; 1665 | } else if (type === "Boolean") { 1666 | typecastValue = `Boolean(${typecastValue})`; 1667 | } else if (type === "DateTime") { 1668 | typecastValue = `new Date(String(${typecastValue}))`; 1669 | } else { 1670 | typecastValue = `String(${typecastValue})`; 1671 | } 1672 | } 1673 | return ` ${name}: ${nonTypecastValue} ? ${typecastValue} : undefined,`; 1674 | } 1675 | ); 1676 | 1677 | // Convert fields pointing to other relations differently 1678 | modelTree.model.fields.map((field) => { 1679 | if (field.kind === "object") { 1680 | const relationFrom = field.relationFromFields && field.relationFromFields; 1681 | const referencedModelName = field.type; 1682 | 1683 | if (referencedModelName === modelTree.parent?.name) { 1684 | } else if (relationFrom) { 1685 | relationFrom.forEach((rf) => { 1686 | const fieldType2 = modelTree.model.fields.find( 1687 | (f) => f.name === rf 1688 | )?.type; 1689 | const nonTypecastValue = `formData.get('${relationFrom}')`; 1690 | 1691 | let typecastValue = nonTypecastValue; 1692 | if (fieldType2 === "Int" || fieldType2 === "Float") { 1693 | typecastValue = `Number(${typecastValue})`; 1694 | } else if (fieldType2 === "Boolean") { 1695 | typecastValue = `Boolean(${typecastValue})`; 1696 | } else if (fieldType2 === "DateTime") { 1697 | typecastValue = `new Date(String(${typecastValue}))`; 1698 | } else { 1699 | typecastValue = `String(${typecastValue})`; 1700 | } 1701 | 1702 | convertToPrismaInputLines.push( 1703 | ` ${relationFrom}: ${nonTypecastValue} ? ${typecastValue} : undefined,` 1704 | ); 1705 | }); 1706 | } 1707 | } 1708 | }); 1709 | 1710 | return `{ 1711 | ${convertToPrismaInputLines.join("\n")} 1712 | }`; 1713 | } 1714 | 1715 | export function generateWhereParentClause( 1716 | inputObject: string | undefined, 1717 | fieldAccessValue: string | undefined, 1718 | parentIdentifierFieldName: string | undefined, 1719 | parentIdentifierFieldType: string | undefined, 1720 | parentReferenceField: string | undefined, 1721 | manyToManyWhere: string, 1722 | selectClause: string 1723 | ): string { 1724 | if (manyToManyWhere == "") { 1725 | if ( 1726 | inputObject && 1727 | fieldAccessValue && 1728 | parentIdentifierFieldName && 1729 | parentIdentifierFieldType && 1730 | parentReferenceField 1731 | ) { 1732 | let typecastValue = `${inputObject}.${fieldAccessValue}`; 1733 | if ( 1734 | parentIdentifierFieldType === "Int" || 1735 | parentIdentifierFieldType === "Float" 1736 | ) { 1737 | typecastValue = `Number(${typecastValue})`; 1738 | } else if (parentIdentifierFieldType === "Boolean") { 1739 | typecastValue = `Boolean(${typecastValue})`; 1740 | } else if (parentIdentifierFieldType === "String") { 1741 | typecastValue = `String(${typecastValue})`; 1742 | } 1743 | return `({ where: { ${parentReferenceField}: {${parentIdentifierFieldName}: {equals: ${typecastValue}} }, }, ${selectClause} })`; 1744 | } else { 1745 | return `{${selectClause}}`; 1746 | } 1747 | } else { 1748 | return `({ ${manyToManyWhere} , ${selectClause}}, )`; 1749 | } 1750 | } 1751 | 1752 | export function generateWhereClause( 1753 | inputObject: string | undefined, 1754 | uniqueDynamicSlugs: string[], 1755 | modelUniqueIdFields: { 1756 | name: string; 1757 | type: string; 1758 | }[] 1759 | ): string { 1760 | let returnClause = ""; 1761 | if (inputObject && modelUniqueIdFields) { 1762 | if (modelUniqueIdFields.length > 1) { 1763 | returnClause += 1764 | "{" + modelUniqueIdFields.map((f) => f.name).join("_") + ":{"; 1765 | modelUniqueIdFields.forEach((f, index) => { 1766 | let typecastValue = `${inputObject}.${uniqueDynamicSlugs[index]}`; 1767 | if (f.type === "Int" || f.type === "Float") { 1768 | typecastValue = `Number(${typecastValue})`; 1769 | } else if (f.type === "Boolean") { 1770 | typecastValue = `Boolean(${typecastValue})`; 1771 | } else if (f.type === "String") { 1772 | typecastValue = `String(${typecastValue})`; 1773 | } 1774 | 1775 | returnClause += `${f.name}: ${typecastValue} ,`; 1776 | }); 1777 | returnClause += "}},"; 1778 | } else { 1779 | const { name, type } = modelUniqueIdFields[0]; 1780 | let typecastValue = `${inputObject}.${uniqueDynamicSlugs[0]}`; 1781 | if (type === "Int" || type === "Float") { 1782 | typecastValue = `Number(${typecastValue})`; 1783 | } else if (type === "Boolean") { 1784 | typecastValue = `Boolean(${typecastValue})`; 1785 | } else if (type === "String") { 1786 | typecastValue = `String(${typecastValue})`; 1787 | } 1788 | 1789 | return `{ ${name}: ${typecastValue} },`; 1790 | } 1791 | return returnClause; 1792 | } else { 1793 | return ""; 1794 | } 1795 | } 1796 | 1797 | export function generateDeleteClause( 1798 | uniqueIdentifierFields: { 1799 | name: string; 1800 | type: string; 1801 | }[] 1802 | ): string { 1803 | if (uniqueIdentifierFields.length === 1) { 1804 | const singleIdField = uniqueIdentifierFields[0]; 1805 | let typecastValue = `formData.get('${singleIdField.name}')`; 1806 | if (singleIdField.type === "Int" || singleIdField.type === "Float") { 1807 | typecastValue = `Number(${typecastValue})`; 1808 | } else if (singleIdField.type === "Boolean") { 1809 | typecastValue = `Boolean(${typecastValue})`; 1810 | } else if (singleIdField.type === "String") { 1811 | typecastValue = `String(${typecastValue})`; 1812 | } 1813 | return `{ ${singleIdField.name}: ${typecastValue} },`; 1814 | } else if (uniqueIdentifierFields.length >= 2) { 1815 | let andClause = "{" + uniqueIdentifierFields.map((f) => f.name).join("_"); 1816 | andClause += ":{"; 1817 | uniqueIdentifierFields.forEach((f) => { 1818 | let typecastValue = `formData.get('${f.name}')`; 1819 | if (f.type === "Int" || f.type === "Float") { 1820 | typecastValue = `Number(${typecastValue})`; 1821 | } else if (f.type === "Boolean") { 1822 | typecastValue = `Boolean(${typecastValue})`; 1823 | } else if (f.type === "String") { 1824 | typecastValue = `String(${typecastValue})`; 1825 | } 1826 | andClause += `${f.name}: ${typecastValue},`; 1827 | }); 1828 | andClause += "}}"; 1829 | return andClause; 1830 | } 1831 | return ""; 1832 | } 1833 | 1834 | // Custom export function to check if field is renderable in a form 1835 | export function isFieldRenderable(field: DMMF.Field): boolean { 1836 | return !(field.isReadOnly || field.isList || !!field.relationName); 1837 | } 1838 | export function generateFormFields( 1839 | modelTree: ModelTree, 1840 | enums: Record<string, string[]> 1841 | ): string { 1842 | return modelTree.model.fields 1843 | .map((field) => { 1844 | const required = 1845 | field.isRequired && !field.hasDefaultValue ? "required" : ""; 1846 | 1847 | const checkboxStyle = 1848 | field.type === "Boolean" ? "text-sky-700" : "text-slate-300 w-full"; 1849 | 1850 | // Enum 1851 | if (field.kind === "enum") { 1852 | const enumValues = enums[field.type]; 1853 | return `<label className="${labelClass}">${field.name} ${ 1854 | required && "*" 1855 | } </label>\n 1856 | 1857 | <select name="${ 1858 | field.name 1859 | }" className="${inputClass} ${checkboxStyle}" id="${field.name}"> 1860 | ${enumValues.map((v) => `<option value="${v}">${v}</option>`)} 1861 | </select>`; 1862 | } 1863 | 1864 | const inputType = prismaFieldToInputType[field.type] || "text"; 1865 | 1866 | if (field.kind === "object") { 1867 | const relationFrom = 1868 | field.relationFromFields && field.relationFromFields[0]; 1869 | 1870 | const referencedModelName = field.type; 1871 | 1872 | if (referencedModelName === modelTree.parent?.name) { 1873 | } else if (relationFrom) { 1874 | const fieldType2 = modelTree.model.fields.find( 1875 | (f) => f.name === relationFrom 1876 | )?.type; 1877 | if (fieldType2) { 1878 | const inputType2 = prismaFieldToInputType[fieldType2] || "text"; 1879 | 1880 | return `<label className="${labelClass}">${relationFrom} ${ 1881 | required && "*" 1882 | }</label>\n 1883 | <input type="${inputType2}" name="${relationFrom}" 1884 | className=" ${inputClass} ${checkboxStyle}" 1885 | ${required}/>`; 1886 | } else { 1887 | return ""; 1888 | } 1889 | } 1890 | } 1891 | 1892 | let returnValue = ""; 1893 | if (isFieldRenderable(field)) { 1894 | returnValue = `<label className="${labelClass}">${field.name} ${ 1895 | required && "*" 1896 | }</label>\n 1897 | <input type="${inputType}" ${ 1898 | field.type === "Float" ? 'step="0.01"' : "" 1899 | } name="${field.name}" 1900 | className="${inputClass} ${checkboxStyle}" ${required}/>`; 1901 | } 1902 | 1903 | return returnValue; 1904 | }) 1905 | .join("\n"); 1906 | } 1907 | 1908 | export function generateFormFieldsWithDefaults( 1909 | tableFields: DMMF.Field[], 1910 | enums: Record<string, string[]> 1911 | ): string { 1912 | return tableFields 1913 | .map((field) => { 1914 | if (!isFieldRenderable(field)) { 1915 | return ""; 1916 | } 1917 | const required = 1918 | field.isRequired && !field.hasDefaultValue ? "required" : ""; 1919 | const checkboxStyle = 1920 | field.type === "Boolean" ? "text-sky-700" : "text-slate-300 w-full "; 1921 | // Enum 1922 | if (field.kind === "enum") { 1923 | const enumValues = enums[field.type]; 1924 | return `<label className="${labelClass}">${field.name} </label>\n 1925 | <select className="${inputClass} ${checkboxStyle}" name="${ 1926 | field.name 1927 | } ${required && "*"}" id="${ 1928 | field.name 1929 | }" defaultValue={nexquikTemplateModel?.${field.name}}> 1930 | ${enumValues.map((v) => `<option value="${v}">${v}</option>`)} 1931 | </select>`; 1932 | } 1933 | const inputType = prismaFieldToInputType[field.type] || "text"; 1934 | const defaultValue = field.isId 1935 | ? `{nexquikTemplateModel?.${field.name} || 'N/A'}` 1936 | : field.type === "Number" 1937 | ? `{Number(nexquikTemplateModel?.${field.name})}` 1938 | : field.type === "DateTime" 1939 | ? `{nexquikTemplateModel?.${field.name}.toISOString().slice(0, 16)}` 1940 | : `{String(nexquikTemplateModel?.${field.name})}`; 1941 | const disabled = field.isId ? "disabled" : ""; 1942 | 1943 | return `<label className="${labelClass}">${field.name} ${ 1944 | required && "*" 1945 | }</label>\n<input 1946 | className="${ 1947 | disabled === "disabled" ? disabledInputClass : inputClass 1948 | } ${checkboxStyle}" type="${inputType}" name="${field.name}" ${ 1949 | field.type === "Boolean" 1950 | ? `defaultChecked={nexquikTemplateModel?.${field.name} ? nexquikTemplateModel.${field.name} : undefined}` 1951 | : `defaultValue=${defaultValue}` 1952 | } ${ 1953 | field.type === "Float" ? 'step="0.01"' : "" 1954 | } ${disabled} ${required}/>`; 1955 | }) 1956 | .join("\n"); 1957 | } 1958 | --------------------------------------------------------------------------------