Skip to content

Commit aefa4b3

Browse files
authored
Merge pull request #48 from devforth/npx-adminforth-create-app
Npx adminforth create app
2 parents bbf7199 + a87872c commit aefa4b3

File tree

9 files changed

+2252
-51
lines changed

9 files changed

+2252
-51
lines changed

adminforth/commands/cli.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
const args = process.argv.slice(2);
44
const command = args[0];
55

6-
import generateModels from "./generateModels.js";
76
import bundle from "./bundle.js";
7+
import createApp from "./createApp/main.js";
8+
import generateModels from "./generateModels.js";
89

910
switch (command) {
11+
case "create-app":
12+
createApp(args);
13+
break;
1014
case "generate-models":
1115
generateModels();
1216
break;
1317
case "bundle":
1418
bundle();
1519
break;
1620
default:
17-
console.log("Unknown command. Available commands: generate-models, bundle");
18-
}
21+
console.log("Unknown command. Available commands: create-app, generate-models, bundle");
22+
}
2.48 KB
Loading
Lines changed: 19 additions & 0 deletions
Loading
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import chalk from 'chalk';
2+
3+
import {
4+
parseArgumentsIntoOptions,
5+
prepareWorkflow,
6+
promptForMissingOptions,
7+
} from './utils.js';
8+
9+
10+
export default async function createApp(args) {
11+
// Step 1: Parse CLI arguments with `arg`
12+
let options = parseArgumentsIntoOptions(args);
13+
14+
// Step 2: Ask for missing arguments via `inquirer`
15+
options = await promptForMissingOptions(options);
16+
17+
// Step 3: Prepare a Listr-based workflow
18+
const tasks = prepareWorkflow(options)
19+
20+
// Step 4: Run tasks
21+
try {
22+
await tasks.run();
23+
} catch (err) {
24+
console.error(chalk.red(`\n❌ ${err.message}\n`));
25+
process.exit(1);
26+
}
27+
}
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
export function gitignore() {
2+
return `# Dependency directories
3+
node_modules/
4+
5+
# dotenv environment variable files
6+
.env
7+
`;
8+
}
9+
10+
export function env(dbUrl, prismaDbUrl) {
11+
const base = `
12+
ADMINFORTH_SECRET=123
13+
NODE_ENV=development
14+
DATABASE_URL=${dbUrl}
15+
`
16+
if (prismaDbUrl)
17+
return base + `PRISMA_DATABASE_URL=${prismaDbUrl}`;
18+
return base;
19+
}
20+
21+
export function envSample(dbUrl, prismaDbUrl) {
22+
return env(dbUrl, prismaDbUrl);
23+
}
24+
25+
export function indexTs(appName) {
26+
return `import express from 'express';
27+
import AdminForth, { Filters } from 'adminforth';
28+
import usersResource from "./resources/users";
29+
30+
const ADMIN_BASE_URL = '';
31+
32+
export const admin = new AdminForth({
33+
baseUrl : ADMIN_BASE_URL,
34+
auth: {
35+
usersResourceId: 'users', // resource to get user during login
36+
usernameField: 'email', // field where username is stored, should exist in resource
37+
passwordHashField: 'password_hash',
38+
rememberMeDays: 30, // users who will check "remember me" will stay logged in for 30 days
39+
loginBackgroundImage: 'https://images.unsplash.com/photo-1534239697798-120952b76f2b?q=80&w=3389&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
40+
loginBackgroundPosition: '1/2', // over, 3/4, 2/5, 3/5 (tailwind grid)
41+
demoCredentials: "adminforth:adminforth", // never use it for production
42+
loginPromptHTML: "Use email <b>adminforth</b> and password <b>adminforth</b> to login",
43+
},
44+
customization: {
45+
brandName: '${appName}',
46+
title: '${appName}',
47+
favicon: '@@/assets/favicon.png',
48+
brandLogo: '@@/assets/logo.svg',
49+
datesFormat: 'DD MMM',
50+
timeFormat: 'HH:mm a',
51+
showBrandNameInSidebar: true,
52+
styles: {
53+
colors: {
54+
light: {
55+
// color for links, icons etc.
56+
primary: '#B400B8',
57+
// color for sidebar and text
58+
sidebar: {main:'#571E58', text:'white'},
59+
},
60+
dark: {
61+
primary: '#82ACFF',
62+
sidebar: {main:'#1f2937', text:'#9ca3af'},
63+
}
64+
}
65+
},
66+
},
67+
dataSources: [
68+
{
69+
id: 'maindb',
70+
url: \`\${process.env.DATABASE_URL}\`
71+
},
72+
],
73+
resources: [
74+
usersResource,
75+
],
76+
menu: [
77+
{
78+
type: 'heading',
79+
label: 'SYSTEM',
80+
},
81+
{
82+
label: 'Users',
83+
icon: 'flowbite:user-solid', // any icon from iconify supported in format <setname>:<icon>, e.g. from here https://icon-sets.iconify.design/flowbite/
84+
resourceId: 'users',
85+
}
86+
],
87+
});
88+
89+
if (import.meta.url === \`file://\${process.argv[1]}\`) {
90+
// if script is executed directly e.g. node index.ts or npm start
91+
92+
const app = express()
93+
app.use(express.json());
94+
const port = 3500;
95+
96+
// needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime
97+
await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});
98+
console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');
99+
100+
// serve after you added all api
101+
admin.express.serve(app)
102+
103+
admin.discoverDatabases().then(async () => {
104+
if (!await admin.resource('users').get([Filters.EQ('email', 'adminforth')])) {
105+
await admin.resource('users').create({
106+
email: 'adminforth',
107+
password_hash: await AdminForth.Utils.generatePasswordHash('adminforth'),
108+
role: 'superadmin',
109+
});
110+
}
111+
});
112+
113+
admin.express.listen(port, () => {
114+
console.log(\`Example app listening at http://localhost:\${port}\`)
115+
console.log(\`\\n⚡ AdminForth is available at http://localhost:\${port}\${ADMIN_BASE_URL}\n\`)
116+
});
117+
}
118+
`;
119+
}
120+
121+
export function schemaPrisma(provider, dbUrl) {
122+
return `generator client {
123+
provider = "prisma-client-js"
124+
}
125+
126+
datasource db {
127+
provider = "${provider}"
128+
url = env("PRISMA_DATABASE_URL")
129+
}
130+
131+
model adminuser {
132+
id String @id
133+
email String @unique
134+
password_hash String
135+
role String
136+
created_at DateTime
137+
}
138+
`;
139+
}
140+
141+
export function usersResource() {
142+
return `import AdminForth, { AdminForthDataTypes, AdminForthResourceInput } from 'adminforth';
143+
import type { AdminUser } from 'adminforth';
144+
145+
async function canModifyUsers({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
146+
return adminUser.dbUser.role === 'superadmin';
147+
}
148+
149+
export default {
150+
dataSource: 'maindb',
151+
table: 'adminuser',
152+
resourceId: 'users',
153+
label: 'Users',
154+
recordLabel: (r) => \`👤 \${r.email}\`,
155+
options: {
156+
allowedActions: {
157+
edit: canModifyUsers,
158+
delete: canModifyUsers,
159+
},
160+
},
161+
columns: [
162+
{
163+
name: 'id',
164+
primaryKey: true,
165+
type: AdminForthDataTypes.STRING,
166+
fillOnCreate: ({ initialRecord, adminUser }) => Math.random().toString(36).substring(7),
167+
showIn: ['list', 'filter', 'show'],
168+
},
169+
{
170+
name: 'email',
171+
required: true,
172+
isUnique: true,
173+
type: AdminForthDataTypes.STRING,
174+
validation: [
175+
// you can also use AdminForth.Utils.EMAIL_VALIDATOR which is alias to this object
176+
{
177+
regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
178+
message: 'Email is not valid, must be in format example@test.com'
179+
},
180+
]
181+
},
182+
{
183+
name: 'created_at',
184+
type: AdminForthDataTypes.DATETIME,
185+
showIn: ['list', 'filter', 'show'],
186+
fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),
187+
},
188+
{
189+
name: 'role',
190+
type: AdminForthDataTypes.STRING,
191+
enum: [
192+
{ value: 'superadmin', label: 'Super Admin' },
193+
{ value: 'user', label: 'User' },
194+
]
195+
},
196+
{
197+
name: 'password',
198+
virtual: true, // field will not be persisted into db
199+
required: { create: true }, // make required only on create page
200+
editingNote: { edit: 'Leave empty to keep password unchanged' },
201+
type: AdminForthDataTypes.STRING,
202+
showIn: ['create', 'edit'], // to show field only on create and edit pages
203+
masked: true, // to show stars in input field
204+
205+
minLength: 8,
206+
validation: [
207+
// request to have at least 1 digit, 1 upper case, 1 lower case
208+
AdminForth.Utils.PASSWORD_VALIDATORS.UP_LOW_NUM,
209+
],
210+
},
211+
{
212+
name: 'password_hash',
213+
type: AdminForthDataTypes.STRING,
214+
backendOnly: true,
215+
showIn: []
216+
}
217+
],
218+
hooks: {
219+
create: {
220+
beforeSave: async ({ record, adminUser, resource }) => {
221+
record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);
222+
return { ok: true };
223+
}
224+
},
225+
edit: {
226+
beforeSave: async ({ record, adminUser, resource }) => {
227+
if (record.password) {
228+
record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password);
229+
}
230+
return { ok: true }
231+
},
232+
},
233+
}
234+
} as AdminForthResourceInput;
235+
`;
236+
}
237+
238+
/**
239+
* Root package.json template
240+
*/
241+
export function rootPackageJson(appName) {
242+
// Note: you might want to keep versions in sync or fetch from a config
243+
return `{
244+
"name": "${appName}",
245+
"version": "1.0.0",
246+
"main": "index.ts",
247+
"type": "module",
248+
"keywords": [],
249+
"author": "",
250+
"license": "ISC",
251+
"description": "",
252+
"scripts": {
253+
"start": "tsx watch --env-file=.env index.ts",
254+
"migrate": "npx prisma migrate deploy",
255+
"makemigration": "npx --yes prisma migrate deploy; npx --yes prisma migrate dev",
256+
"test": "echo \\"Error: no test specified\\" && exit 1"
257+
},
258+
"engines": {
259+
"node": ">=20"
260+
},
261+
"dependencies": {
262+
"adminforth": "latest",
263+
"express": "latest"
264+
},
265+
"devDependencies": {
266+
"typescript": "5.4.5",
267+
"tsx": "4.11.2",
268+
"@types/express": "latest",
269+
"@types/node": "latest"
270+
}
271+
}
272+
`;
273+
}
274+
275+
/**
276+
* Root tsconfig.json template
277+
*/
278+
export function rootTsConfig() {
279+
return `{
280+
"compilerOptions": {
281+
"target": "esnext",
282+
"module": "nodenext",
283+
"esModuleInterop": true,
284+
"forceConsistentCasingInFileNames": true,
285+
"strict": true
286+
},
287+
"exclude": ["node_modules", "dist"]
288+
}
289+
`;
290+
}
291+
292+
/**
293+
* custom/package.json
294+
*/
295+
export function customPackageJson(appName) {
296+
return `{
297+
"name": "custom",
298+
"version": "1.0.0",
299+
"main": "index.ts",
300+
"scripts": {
301+
"test": "echo \\"Error: no test specified\\" && exit 1"
302+
},
303+
"keywords": [],
304+
"author": "",
305+
"license": "ISC",
306+
"description": ""
307+
}
308+
`;
309+
}
310+
311+
/**
312+
* custom/tsconfig.json
313+
*/
314+
export function customTsconfig() {
315+
return `{
316+
"compilerOptions": {
317+
"baseUrl": ".",
318+
"paths": {
319+
"@/": "../node_modules/adminforth/dist/spa/src/",
320+
"": "../node_modules/adminforth/dist/spa/node_modules/",
321+
"@@/*": "."
322+
}
323+
}
324+
}
325+
`;
326+
}

0 commit comments

Comments
 (0)