create-efc-app 0.4.13 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +174 -830
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ async function scaffold(opts) {
|
|
|
41
41
|
await writeEfcConfig(dest, opts);
|
|
42
42
|
await writeEntryPoint(dest, opts);
|
|
43
43
|
await writeGitignore(dest);
|
|
44
|
+
await writeAgentsMd(dest, opts);
|
|
44
45
|
await writeEnvFiles(dest, opts);
|
|
45
46
|
await writeExampleRoute(dest, opts);
|
|
46
47
|
if (needsUserModel) await writeUserModel(dest, opts);
|
|
@@ -70,31 +71,33 @@ async function scaffold(opts) {
|
|
|
70
71
|
if (opts.userPortal && uf.apiAndBilling) await writeUserBillingRoutes(dest, opts);
|
|
71
72
|
if (opts.userPortal && uf.support) await writeSupportRoutes(dest, opts);
|
|
72
73
|
if (opts.adminPortal) await writeAdminExtendedRoutes(dest, opts);
|
|
74
|
+
await writeInitMigration(dest, opts);
|
|
73
75
|
}
|
|
74
76
|
async function writePackageJson(dest, opts) {
|
|
75
77
|
const deps = {
|
|
76
|
-
"express-file-cluster": "^0.
|
|
78
|
+
"express-file-cluster": "^0.5.0"
|
|
77
79
|
};
|
|
78
|
-
if (opts.database === "mongodb") deps["mongoose"] = "^
|
|
80
|
+
if (opts.database === "mongodb") deps["mongoose"] = "^9.7.2";
|
|
79
81
|
if (opts.database === "postgresql") {
|
|
80
|
-
deps["pg"] = "^8.
|
|
81
|
-
deps["drizzle-orm"] = "^0.
|
|
82
|
+
deps["pg"] = "^8.11.0";
|
|
83
|
+
deps["drizzle-orm"] = "^0.45.0";
|
|
82
84
|
}
|
|
83
85
|
if (opts.tasks && opts.taskBackend === "bullmq") deps["bullmq"] = "^5.0.0";
|
|
84
|
-
if (opts.tasks && opts.taskBackend === "pg-boss") deps["pg-boss"] = "^
|
|
86
|
+
if (opts.tasks && opts.taskBackend === "pg-boss") deps["pg-boss"] = "^12.0.0";
|
|
85
87
|
if (opts.mailer) deps["nodemailer"] = "^6.9.0";
|
|
86
|
-
|
|
88
|
+
deps["bcrypt"] = "^5.1.0";
|
|
87
89
|
const devDeps = {
|
|
88
|
-
vitest: "^
|
|
90
|
+
vitest: "^5.0.3"
|
|
89
91
|
};
|
|
90
92
|
if (opts.language === "typescript") {
|
|
93
|
+
devDeps["tsup"] = "^8.2.0";
|
|
91
94
|
devDeps["typescript"] = "^5.5.0";
|
|
92
95
|
devDeps["@types/node"] = "^22.0.0";
|
|
93
96
|
devDeps["@types/express"] = "^4.17.21";
|
|
94
|
-
devDeps["tsup"] = "^8.2.0";
|
|
95
97
|
devDeps["tsx"] = "^4.0.0";
|
|
96
98
|
if (opts.mailer) devDeps["@types/nodemailer"] = "^6.4.0";
|
|
97
|
-
|
|
99
|
+
devDeps["@types/bcrypt"] = "^5.0.0";
|
|
100
|
+
if (opts.database === "postgresql") devDeps["@types/pg"] = "^8.11.0";
|
|
98
101
|
}
|
|
99
102
|
const pkg = {
|
|
100
103
|
name: opts.projectName,
|
|
@@ -104,7 +107,11 @@ async function writePackageJson(dest, opts) {
|
|
|
104
107
|
dev: "efc start dev",
|
|
105
108
|
build: "efc build prod",
|
|
106
109
|
start: "efc start prod",
|
|
107
|
-
test: "efc run tests"
|
|
110
|
+
test: "efc run tests",
|
|
111
|
+
"db:migrate": "efc db migrate",
|
|
112
|
+
"db:rollback": "efc db rollback",
|
|
113
|
+
"db:seed": "efc db seed",
|
|
114
|
+
"db:studio": "efc db studio"
|
|
108
115
|
},
|
|
109
116
|
dependencies: deps,
|
|
110
117
|
devDependencies: devDeps
|
|
@@ -125,7 +132,7 @@ async function writeTsConfig(dest, opts) {
|
|
|
125
132
|
},
|
|
126
133
|
// No explicit rootDir: efc.config.ts lives at the project root, outside src/, and
|
|
127
134
|
// src/index.ts imports it. `efc build prod` only uses this config for `tsc --noEmit`
|
|
128
|
-
// typechecking — the actual dist/
|
|
135
|
+
// typechecking — tsup writes the actual output (dist/src/**, dist/efc.config.js),
|
|
129
136
|
// so leaving rootDir implicit here has no effect on the production build's output layout.
|
|
130
137
|
include: ["src/**/*", "efc.config.ts"],
|
|
131
138
|
exclude: ["node_modules", "dist"]
|
|
@@ -134,17 +141,18 @@ async function writeTsConfig(dest, opts) {
|
|
|
134
141
|
}
|
|
135
142
|
async function writeEfcConfig(dest, opts) {
|
|
136
143
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
137
|
-
const tasks = opts.tasks ?
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
const tasks = !opts.tasks ? "false" : opts.taskBackend === "pg-boss" ? `{ backend: 'pg-boss', concurrency: 5 }` : `{ backend: 'bullmq', concurrency: 5, redisUrl: process.env.REDIS_URL }`;
|
|
145
|
+
const ts = opts.language === "typescript";
|
|
146
|
+
const typeImport = ts ? "import type { EFCConfig } from 'express-file-cluster';\n\n" : "";
|
|
147
|
+
const declaration = ts ? "const config: EFCConfig = {" : "/** @type {import('express-file-cluster').EFCConfig} */\nconst config = {";
|
|
148
|
+
const content = `${typeImport}// The framework never reads process.env itself \u2014 every runtime value it needs is read
|
|
141
149
|
// here, explicitly, and passed in. Edit .env to change values; edit this file to change
|
|
142
150
|
// which env vars are wired up or add new ones.
|
|
143
151
|
const corsOrigins = process.env.CORS_ORIGINS
|
|
144
152
|
? process.env.CORS_ORIGINS.split(',').map((o) => o.trim()).filter(Boolean)
|
|
145
153
|
: undefined;
|
|
146
154
|
|
|
147
|
-
|
|
155
|
+
${declaration}
|
|
148
156
|
port: process.env.PORT ? Number(process.env.PORT) : undefined,
|
|
149
157
|
databaseUrl: process.env.DATABASE_URL,
|
|
150
158
|
jwtSecret: process.env.JWT_SECRET,
|
|
@@ -153,6 +161,10 @@ const config: EFCConfig = {
|
|
|
153
161
|
cors: corsOrigins ? { origin: corsOrigins } : true,
|
|
154
162
|
authStrategy: '${opts.authStrategy}',
|
|
155
163
|
tasks: ${tasks},
|
|
164
|
+
// Cache + session-revocation store (logout really invalidates tokens). Redis when
|
|
165
|
+
// REDIS_URL is set \u2014 shared by every instance; otherwise an in-memory store shared by
|
|
166
|
+
// this machine's workers, emptied on restart.
|
|
167
|
+
cache: process.env.REDIS_URL ? { backend: 'redis', url: process.env.REDIS_URL } : { backend: 'memory' },
|
|
156
168
|
globalMiddlewares: [],
|
|
157
169
|
};
|
|
158
170
|
|
|
@@ -162,6 +174,7 @@ export default config;
|
|
|
162
174
|
}
|
|
163
175
|
async function writeEntryPoint(dest, opts) {
|
|
164
176
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
177
|
+
const clusterLine = opts.cluster ? "" : "\n cluster: false,";
|
|
165
178
|
const content = `import { ignite, gracefulShutdown } from 'express-file-cluster';
|
|
166
179
|
import config from '../efc.config.js';
|
|
167
180
|
|
|
@@ -169,8 +182,7 @@ import config from '../efc.config.js';
|
|
|
169
182
|
// .env in efc.config.${ext} and spread in below \u2014 ignite() itself never touches process.env
|
|
170
183
|
// for these, so this object is the single source of truth for what's actually applied.
|
|
171
184
|
ignite({
|
|
172
|
-
...config
|
|
173
|
-
cluster: ${opts.cluster},
|
|
185
|
+
...config,${clusterLine}
|
|
174
186
|
}).then(gracefulShutdown).catch(console.error);
|
|
175
187
|
`;
|
|
176
188
|
await fs.outputFile(path.join(dest, "src", `index.${ext}`), content);
|
|
@@ -181,6 +193,58 @@ async function writeGitignore(dest) {
|
|
|
181
193
|
"node_modules/\ndist/\n.env\n.env.local\n*.log\n"
|
|
182
194
|
);
|
|
183
195
|
}
|
|
196
|
+
async function writeAgentsMd(dest, opts) {
|
|
197
|
+
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
198
|
+
const D = "node_modules/express-file-cluster/dist/docs";
|
|
199
|
+
const engine = opts.database === "postgresql" ? "PostgreSQL" : "MongoDB";
|
|
200
|
+
const content = `# AGENTS.md
|
|
201
|
+
|
|
202
|
+
This is an **express-file-cluster (EFC)** app (${opts.language === "typescript" ? "TypeScript" : "JavaScript"}, ${engine}).
|
|
203
|
+
EFC's API is probably not in your training data \u2014 **read its docs before writing code**, and
|
|
204
|
+
don't use any EFC import, option or method you can't find in them.
|
|
205
|
+
|
|
206
|
+
## Read the docs first
|
|
207
|
+
|
|
208
|
+
The docs for the exact installed version ship inside the package, under \`${D}/\`:
|
|
209
|
+
|
|
210
|
+
1. \`${D}/skills/api-surface.md\` \u2014 every public export and signature
|
|
211
|
+
2. \`${D}/skills/what-not-to-invent.md\` \u2014 things that do NOT exist
|
|
212
|
+
3. \`${D}/index.md\`, then the page for your task:
|
|
213
|
+
- routes & URL params \u2014 \`core-concepts/file-based-routing.md\`, \`skills/routing-rules.md\`
|
|
214
|
+
- request validation \u2014 \`guides/validation.md\`
|
|
215
|
+
- models & queries \u2014 \`guides/database.md\`, \`api-reference/db.md\`${opts.database === "postgresql" ? ", `api-reference/table.md` (PostgreSQL-native `defineTable`)" : ""}
|
|
216
|
+
- migrations \u2014 \`guides/migrations.md\`
|
|
217
|
+
- auth & roles \u2014 \`guides/authentication.md\`, \`guides/rbac.md\`
|
|
218
|
+
- background tasks \u2014 \`core-concepts/background-tasks.md\`
|
|
219
|
+
- config & boot \u2014 \`api-reference/ignite.md\`
|
|
220
|
+
|
|
221
|
+
Search them: \`grep -rn "<term>" ${D}\`
|
|
222
|
+
|
|
223
|
+
Optional \u2014 the same docs over MCP, with search and scaffolding tools:
|
|
224
|
+
\`claude mcp add efc -- npx -y express-file-cluster-mcp\`
|
|
225
|
+
(other clients: command \`npx\`, args \`["-y", "express-file-cluster-mcp"]\`).
|
|
226
|
+
|
|
227
|
+
## Project conventions
|
|
228
|
+
|
|
229
|
+
- \`src/api/**\` \u2014 every file is a route (\`users/[id].${ext}\` \u2192 \`/v1/api/users/:id\`); export
|
|
230
|
+
uppercase \`GET\`/\`POST\`/\u2026 handlers.
|
|
231
|
+
- \`src/tasks/*.${ext}\` \u2014 background tasks; the file name is the task name (not recursive).
|
|
232
|
+
- \`src/model/*.${ext}\` \u2014 \`defineModel\` models.
|
|
233
|
+
- \`src/index.${ext}\` is the entry point; it imports \`efc.config.${ext}\` and passes it to \`ignite()\`.
|
|
234
|
+
- Only three import paths exist: \`express-file-cluster\`, \`express-file-cluster/auth\`,
|
|
235
|
+
\`express-file-cluster/tasks\`.
|
|
236
|
+
|
|
237
|
+
## Commands
|
|
238
|
+
|
|
239
|
+
\`\`\`bash
|
|
240
|
+
npx efc start dev # dev server with reload
|
|
241
|
+
npx efc db migrate # run migrations
|
|
242
|
+
npx efc build prod # type-check + production build
|
|
243
|
+
npx efc doctor # diagnose the setup
|
|
244
|
+
\`\`\`
|
|
245
|
+
`;
|
|
246
|
+
await fs.outputFile(path.join(dest, "AGENTS.md"), content);
|
|
247
|
+
}
|
|
184
248
|
async function writeEnvFiles(dest, opts) {
|
|
185
249
|
const secret = crypto.randomBytes(64).toString("hex");
|
|
186
250
|
const projectName = path.basename(dest);
|
|
@@ -206,17 +270,16 @@ SMTP_USER=your@email.com
|
|
|
206
270
|
SMTP_PASS=${isGmail ? "your_16_char_app_password" : "your_smtp_password"}${passComment}
|
|
207
271
|
SMTP_FROM=noreply@yourapp.com
|
|
208
272
|
` : "";
|
|
273
|
+
const redis = opts.tasks && opts.taskBackend !== "pg-boss" ? "\nREDIS_URL=redis://localhost:6379" : "";
|
|
209
274
|
const dotenv = `PORT=3000
|
|
210
275
|
NODE_ENV=development
|
|
211
276
|
DATABASE_URL=${dbUrl}
|
|
212
|
-
JWT_SECRET=${secret}
|
|
213
|
-
REDIS_URL=redis://localhost:6379
|
|
277
|
+
JWT_SECRET=${secret}${redis}
|
|
214
278
|
CORS_ORIGINS=http://localhost:3000${smtpVars}`;
|
|
215
279
|
const example = `PORT=3000
|
|
216
280
|
NODE_ENV=development
|
|
217
281
|
DATABASE_URL=${dbExampleUrl}
|
|
218
|
-
JWT_SECRET=<generate with: openssl rand -hex 64
|
|
219
|
-
REDIS_URL=redis://localhost:6379
|
|
282
|
+
JWT_SECRET=<generate with: openssl rand -hex 64>${redis}
|
|
220
283
|
CORS_ORIGINS=http://localhost:3000,https://yourapp.com${smtpExample}`;
|
|
221
284
|
await fs.outputFile(path.join(dest, ".env"), dotenv);
|
|
222
285
|
await fs.outputFile(path.join(dest, ".env.example"), example);
|
|
@@ -328,39 +391,7 @@ export default defineTask(async (payload) => {
|
|
|
328
391
|
}
|
|
329
392
|
async function writeUserModel(dest, opts) {
|
|
330
393
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
331
|
-
const
|
|
332
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
333
|
-
|
|
334
|
-
export interface UserDocument {
|
|
335
|
-
name: string;
|
|
336
|
-
email: string;
|
|
337
|
-
password: string;
|
|
338
|
-
role: string;
|
|
339
|
-
avatar?: string;
|
|
340
|
-
isVerified: boolean;
|
|
341
|
-
isActive: boolean;
|
|
342
|
-
verifyToken?: string;
|
|
343
|
-
resetToken?: string;
|
|
344
|
-
resetTokenExpiry?: Date;
|
|
345
|
-
refreshToken?: string;
|
|
346
|
-
refreshTokenExpiry?: Date;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export const User = defineModel<UserDocument>('User', {
|
|
350
|
-
name: { type: 'string', required: true },
|
|
351
|
-
email: { type: 'string', required: true, unique: true },
|
|
352
|
-
password: { type: 'string', required: true },
|
|
353
|
-
role: { type: 'string', required: true, default: 'user' },
|
|
354
|
-
avatar: { type: 'string' },
|
|
355
|
-
isVerified: { type: 'boolean', default: false },
|
|
356
|
-
isActive: { type: 'boolean', default: true },
|
|
357
|
-
verifyToken: { type: 'string' },
|
|
358
|
-
resetToken: { type: 'string' },
|
|
359
|
-
resetTokenExpiry: { type: 'date' },
|
|
360
|
-
refreshToken: { type: 'string' },
|
|
361
|
-
refreshTokenExpiry: { type: 'date' },
|
|
362
|
-
});
|
|
363
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
394
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
364
395
|
|
|
365
396
|
export const User = defineModel('User', {
|
|
366
397
|
name: { type: 'string', required: true },
|
|
@@ -376,88 +407,25 @@ export const User = defineModel('User', {
|
|
|
376
407
|
refreshToken: { type: 'string' },
|
|
377
408
|
refreshTokenExpiry: { type: 'date' },
|
|
378
409
|
});
|
|
379
|
-
` : ts ? `// TODO: define your Drizzle schema for User
|
|
380
|
-
// import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
381
|
-
//
|
|
382
|
-
// export const users = pgTable('users', {
|
|
383
|
-
// id: serial('id').primaryKey(),
|
|
384
|
-
// name: text('name').notNull(),
|
|
385
|
-
// email: text('email').notNull().unique(),
|
|
386
|
-
// password: text('password').notNull(),
|
|
387
|
-
// role: text('role').notNull().default('user'),
|
|
388
|
-
// avatar: text('avatar'),
|
|
389
|
-
// isVerified: boolean('is_verified').notNull().default(false),
|
|
390
|
-
// isActive: boolean('is_active').notNull().default(true),
|
|
391
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
392
|
-
// updatedAt: timestamp('updated_at').defaultNow(),
|
|
393
|
-
// });
|
|
394
|
-
export {};
|
|
395
|
-
` : `// TODO: define your Drizzle schema for User
|
|
396
|
-
export {};
|
|
397
410
|
`;
|
|
398
411
|
await fs.outputFile(path.join(dest, "src", "model", `User.${ext}`), content);
|
|
399
412
|
}
|
|
400
413
|
async function writeAdminModel(dest, opts) {
|
|
401
414
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
402
|
-
const
|
|
403
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
404
|
-
|
|
405
|
-
export interface AdminDocument {
|
|
406
|
-
name: string;
|
|
407
|
-
email: string;
|
|
408
|
-
password: string;
|
|
409
|
-
role: string;
|
|
410
|
-
permissions: string[];
|
|
411
|
-
isActive: boolean;
|
|
412
|
-
resetToken?: string;
|
|
413
|
-
resetTokenExpiry?: Date;
|
|
414
|
-
refreshToken?: string;
|
|
415
|
-
refreshTokenExpiry?: Date;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
export const Admin = defineModel<AdminDocument>('Admin', {
|
|
419
|
-
name: { type: 'string', required: true },
|
|
420
|
-
email: { type: 'string', required: true, unique: true },
|
|
421
|
-
password: { type: 'string', required: true },
|
|
422
|
-
role: { type: 'string', required: true, default: 'admin' },
|
|
423
|
-
permissions: { type: 'array', default: [] },
|
|
424
|
-
isActive: { type: 'boolean', default: true },
|
|
425
|
-
resetToken: { type: 'string' },
|
|
426
|
-
resetTokenExpiry: { type: 'date' },
|
|
427
|
-
refreshToken: { type: 'string' },
|
|
428
|
-
refreshTokenExpiry: { type: 'date' },
|
|
429
|
-
});
|
|
430
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
415
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
431
416
|
|
|
432
417
|
export const Admin = defineModel('Admin', {
|
|
433
418
|
name: { type: 'string', required: true },
|
|
434
419
|
email: { type: 'string', required: true, unique: true },
|
|
435
420
|
password: { type: 'string', required: true },
|
|
436
421
|
role: { type: 'string', required: true, default: 'admin' },
|
|
437
|
-
permissions: { type: 'array', default: [] },
|
|
422
|
+
permissions: { type: 'array', of: 'string', default: [] },
|
|
438
423
|
isActive: { type: 'boolean', default: true },
|
|
439
424
|
resetToken: { type: 'string' },
|
|
440
425
|
resetTokenExpiry: { type: 'date' },
|
|
441
426
|
refreshToken: { type: 'string' },
|
|
442
427
|
refreshTokenExpiry: { type: 'date' },
|
|
443
428
|
});
|
|
444
|
-
` : ts ? `// TODO: define your Drizzle schema for Admin
|
|
445
|
-
// import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
446
|
-
//
|
|
447
|
-
// export const admins = pgTable('admins', {
|
|
448
|
-
// id: serial('id').primaryKey(),
|
|
449
|
-
// name: text('name').notNull(),
|
|
450
|
-
// email: text('email').notNull().unique(),
|
|
451
|
-
// password: text('password').notNull(),
|
|
452
|
-
// role: text('role').notNull().default('admin'),
|
|
453
|
-
// permissions: text('permissions').array().notNull().default([]),
|
|
454
|
-
// isActive: boolean('is_active').notNull().default(true),
|
|
455
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
456
|
-
// updatedAt: timestamp('updated_at').defaultNow(),
|
|
457
|
-
// });
|
|
458
|
-
export {};
|
|
459
|
-
` : `// TODO: define your Drizzle schema for Admin
|
|
460
|
-
export {};
|
|
461
429
|
`;
|
|
462
430
|
await fs.outputFile(path.join(dest, "src", "model", `Admin.${ext}`), content);
|
|
463
431
|
}
|
|
@@ -485,8 +453,8 @@ export const meta: RouteMeta = {
|
|
|
485
453
|
` : "";
|
|
486
454
|
const hasUser = opts.userPortal;
|
|
487
455
|
const hasAdmin = opts.adminPortal;
|
|
488
|
-
const
|
|
489
|
-
const loginDbImports =
|
|
456
|
+
const hasModels = hasUser || hasAdmin;
|
|
457
|
+
const loginDbImports = hasModels ? `import bcrypt from 'bcrypt';
|
|
490
458
|
import crypto from 'node:crypto';
|
|
491
459
|
${hasUser ? `import { User } from '../../model/User.js';
|
|
492
460
|
` : ""}${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
@@ -523,7 +491,7 @@ ${hasUser ? `import { User } from '../../model/User.js';
|
|
|
523
491
|
await issueToken(res, { id: user.id, role: user.role, email: user.email });
|
|
524
492
|
await issueRefreshToken(res, User, user.id);
|
|
525
493
|
res.json({ message: 'Logged in' });`;
|
|
526
|
-
const loginContent =
|
|
494
|
+
const loginContent = hasModels ? ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
527
495
|
import type { Request, Response } from 'express';
|
|
528
496
|
${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
529
497
|
|
|
@@ -586,29 +554,31 @@ ${loginMeta}export const POST = async (req, res) => {
|
|
|
586
554
|
|
|
587
555
|
export const meta: RouteMeta = {
|
|
588
556
|
POST: {
|
|
589
|
-
description: '
|
|
557
|
+
description: 'Revoke the current session and clear the auth cookie.',
|
|
590
558
|
response: { status: 200, body: { message: 'Logged out successfully' } },
|
|
591
559
|
},
|
|
592
560
|
};
|
|
593
561
|
|
|
594
562
|
` : `export const meta = {
|
|
595
563
|
POST: {
|
|
596
|
-
description: '
|
|
564
|
+
description: 'Revoke the current session and clear the auth cookie.',
|
|
597
565
|
response: { status: 200, body: { message: 'Logged out successfully' } },
|
|
598
566
|
},
|
|
599
567
|
};
|
|
600
568
|
|
|
601
569
|
` : "";
|
|
602
|
-
const logoutContent = ts ? `import {
|
|
570
|
+
const logoutContent = ts ? `import { revokeSession } from 'express-file-cluster/auth';
|
|
603
571
|
import type { Request, Response } from 'express';
|
|
604
|
-
${logoutMeta}export const POST = async (
|
|
605
|
-
|
|
572
|
+
${logoutMeta}export const POST = async (req: Request, res: Response) => {
|
|
573
|
+
// Rejects this token from now on (not just the cookie) \u2014 see \`cache\` in efc.config.
|
|
574
|
+
await revokeSession(req, res);
|
|
606
575
|
res.clearCookie('efc_refresh_token');
|
|
607
576
|
res.json({ message: 'Logged out successfully' });
|
|
608
577
|
};
|
|
609
|
-
` : `import {
|
|
610
|
-
${logoutMeta}export const POST = async (
|
|
611
|
-
|
|
578
|
+
` : `import { revokeSession } from 'express-file-cluster/auth';
|
|
579
|
+
${logoutMeta}export const POST = async (req, res) => {
|
|
580
|
+
// Rejects this token from now on (not just the cookie) \u2014 see \`cache\` in efc.config.
|
|
581
|
+
await revokeSession(req, res);
|
|
612
582
|
res.clearCookie('efc_refresh_token');
|
|
613
583
|
res.json({ message: 'Logged out successfully' });
|
|
614
584
|
};
|
|
@@ -635,10 +605,10 @@ export const meta: RouteMeta = {
|
|
|
635
605
|
};
|
|
636
606
|
|
|
637
607
|
` : "";
|
|
638
|
-
const registerDbImports =
|
|
608
|
+
const registerDbImports = `import bcrypt from 'bcrypt';
|
|
639
609
|
import crypto from 'node:crypto';
|
|
640
610
|
${opts.mailer ? "import { enqueue } from 'express-file-cluster/tasks';\n" : ""}import { User } from '../../model/User.js';
|
|
641
|
-
|
|
611
|
+
`;
|
|
642
612
|
const sendVerifyEmail = opts.mailer ? `
|
|
643
613
|
var appUrl = process.env.APP_URL || 'http://localhost:3000';
|
|
644
614
|
await enqueue('SendEmail', {
|
|
@@ -649,7 +619,7 @@ ${opts.mailer ? "import { enqueue } from 'express-file-cluster/tasks';\n" : ""}i
|
|
|
649
619
|
` : `
|
|
650
620
|
// TODO: email this token to the user \u2014 enable the Mailer feature to auto-wire SendEmail
|
|
651
621
|
`;
|
|
652
|
-
const registerContent =
|
|
622
|
+
const registerContent = ts ? `import type { Request, Response } from 'express';
|
|
653
623
|
${registerDbImports}${registerMeta}export const POST = async (req: Request, res: Response) => {
|
|
654
624
|
const { name, email, password } = req.body;
|
|
655
625
|
if (!name || !email || !password) {
|
|
@@ -678,23 +648,6 @@ ${sendVerifyEmail}
|
|
|
678
648
|
const { password: _, verifyToken: __, ...safe } = user;
|
|
679
649
|
res.status(201).json({ message: 'Account created successfully', user: safe });
|
|
680
650
|
};
|
|
681
|
-
` : ts ? `import type { Request, Response } from 'express';
|
|
682
|
-
${registerMeta}export const POST = async (req: Request, res: Response) => {
|
|
683
|
-
const { name, email, password } = req.body;
|
|
684
|
-
if (!name || !email || !password) {
|
|
685
|
-
return res.status(400).json({ error: 'name, email and password are required' });
|
|
686
|
-
}
|
|
687
|
-
// TODO: hash password and persist to DB
|
|
688
|
-
res.status(201).json({ message: 'Account created successfully' });
|
|
689
|
-
};
|
|
690
|
-
` : `${registerMeta}export const POST = async (req, res) => {
|
|
691
|
-
const { name, email, password } = req.body;
|
|
692
|
-
if (!name || !email || !password) {
|
|
693
|
-
return res.status(400).json({ error: 'name, email and password are required' });
|
|
694
|
-
}
|
|
695
|
-
// TODO: hash password and persist to DB
|
|
696
|
-
res.status(201).json({ message: 'Account created successfully' });
|
|
697
|
-
};
|
|
698
651
|
`;
|
|
699
652
|
const meMeta = opts.routeDocs ? ts ? `import type { RouteMeta } from 'express-file-cluster';
|
|
700
653
|
|
|
@@ -765,7 +718,7 @@ export const meta: RouteMeta = {
|
|
|
765
718
|
};
|
|
766
719
|
|
|
767
720
|
` : "";
|
|
768
|
-
const dashboardHasUserModel = opts.
|
|
721
|
+
const dashboardHasUserModel = opts.adminFeatures.userManagement;
|
|
769
722
|
const dashboardDbImport = dashboardHasUserModel ? `import { User } from '../../model/User.js';
|
|
770
723
|
` : "";
|
|
771
724
|
const dashboardContent = dashboardHasUserModel ? ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
@@ -834,12 +787,12 @@ export const meta: RouteMeta = {
|
|
|
834
787
|
};
|
|
835
788
|
|
|
836
789
|
` : "";
|
|
837
|
-
const adminUsersDbImport =
|
|
790
|
+
const adminUsersDbImport = ts ? `import bcrypt from 'bcrypt';
|
|
838
791
|
import { User } from '../../../model/User.js';
|
|
839
792
|
` : `import bcrypt from 'bcrypt';
|
|
840
793
|
import { User } from '../../../model/User.js';
|
|
841
|
-
|
|
842
|
-
const usersListContent =
|
|
794
|
+
`;
|
|
795
|
+
const usersListContent = ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
843
796
|
import type { Request, Response } from 'express';
|
|
844
797
|
${adminUsersDbImport}${usersListMeta}${middlewares}
|
|
845
798
|
export const GET = async (req: Request, res: Response) => {
|
|
@@ -880,33 +833,6 @@ ${roleGuard} const { name, email, password, role } = req.body;
|
|
|
880
833
|
const { password: _, ...safe } = user;
|
|
881
834
|
res.status(201).json({ message: 'User created', user: safe });
|
|
882
835
|
};
|
|
883
|
-
` : ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
884
|
-
import type { Request, Response } from 'express';
|
|
885
|
-
${usersListMeta}${middlewares}
|
|
886
|
-
export const GET = async (_req: Request, res: Response) => {
|
|
887
|
-
${roleGuard} // TODO: fetch users from DB with pagination
|
|
888
|
-
res.json({ users: [], total: 0 });
|
|
889
|
-
};
|
|
890
|
-
|
|
891
|
-
export const POST = async (req: Request, res: Response) => {
|
|
892
|
-
${roleGuard} const { name, email, role } = req.body;
|
|
893
|
-
if (!name || !email) return res.status(400).json({ error: 'name and email are required' });
|
|
894
|
-
// TODO: create user in DB
|
|
895
|
-
res.status(201).json({ message: 'User created', user: { id: 'new-id', name, email, role: role ?? 'user' } });
|
|
896
|
-
};
|
|
897
|
-
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
898
|
-
${usersListMeta}${middlewares}
|
|
899
|
-
export const GET = async (_req, res) => {
|
|
900
|
-
${roleGuard} // TODO: fetch users from DB with pagination
|
|
901
|
-
res.json({ users: [], total: 0 });
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
export const POST = async (req, res) => {
|
|
905
|
-
${roleGuard} const { name, email, role } = req.body;
|
|
906
|
-
if (!name || !email) return res.status(400).json({ error: 'name and email are required' });
|
|
907
|
-
// TODO: create user in DB
|
|
908
|
-
res.status(201).json({ message: 'User created', user: { id: 'new-id', name, email, role: role ?? 'user' } });
|
|
909
|
-
};
|
|
910
836
|
`;
|
|
911
837
|
await fs.outputFile(
|
|
912
838
|
path.join(dest, "src", "api", "admin", "users", `index.${ext}`),
|
|
@@ -951,9 +877,9 @@ export const meta: RouteMeta = {
|
|
|
951
877
|
};
|
|
952
878
|
|
|
953
879
|
` : "";
|
|
954
|
-
const adminUserByIdDbImport =
|
|
955
|
-
|
|
956
|
-
const userByIdContent =
|
|
880
|
+
const adminUserByIdDbImport = `import { User } from '../../../model/User.js';
|
|
881
|
+
`;
|
|
882
|
+
const userByIdContent = ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
957
883
|
import type { Request, Response } from 'express';
|
|
958
884
|
${adminUserByIdDbImport}${userByIdMeta}${middlewares}
|
|
959
885
|
export const GET = async (req: Request, res: Response) => {
|
|
@@ -1006,45 +932,6 @@ ${roleGuard} const { id } = req.params;
|
|
|
1006
932
|
await User.delete(id);
|
|
1007
933
|
res.json({ message: \`User \${id} deleted\` });
|
|
1008
934
|
};
|
|
1009
|
-
` : ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
1010
|
-
import type { Request, Response } from 'express';
|
|
1011
|
-
${userByIdMeta}${middlewares}
|
|
1012
|
-
export const GET = async (req: Request, res: Response) => {
|
|
1013
|
-
${roleGuard} const { id } = req.params;
|
|
1014
|
-
// TODO: fetch user from DB
|
|
1015
|
-
res.json({ user: { id } });
|
|
1016
|
-
};
|
|
1017
|
-
|
|
1018
|
-
export const PUT = async (req: Request, res: Response) => {
|
|
1019
|
-
${roleGuard} const { id } = req.params;
|
|
1020
|
-
// TODO: update user in DB
|
|
1021
|
-
res.json({ message: 'User updated', user: { id, ...req.body } });
|
|
1022
|
-
};
|
|
1023
|
-
|
|
1024
|
-
export const DELETE = async (req: Request, res: Response) => {
|
|
1025
|
-
${roleGuard} const { id } = req.params;
|
|
1026
|
-
// TODO: delete user from DB
|
|
1027
|
-
res.json({ message: \`User \${id} deleted\` });
|
|
1028
|
-
};
|
|
1029
|
-
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
1030
|
-
${userByIdMeta}${middlewares}
|
|
1031
|
-
export const GET = async (req, res) => {
|
|
1032
|
-
${roleGuard} const { id } = req.params;
|
|
1033
|
-
// TODO: fetch user from DB
|
|
1034
|
-
res.json({ user: { id } });
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
export const PUT = async (req, res) => {
|
|
1038
|
-
${roleGuard} const { id } = req.params;
|
|
1039
|
-
// TODO: update user in DB
|
|
1040
|
-
res.json({ message: 'User updated', user: { id, ...req.body } });
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
|
-
export const DELETE = async (req, res) => {
|
|
1044
|
-
${roleGuard} const { id } = req.params;
|
|
1045
|
-
// TODO: delete user from DB
|
|
1046
|
-
res.json({ message: \`User \${id} deleted\` });
|
|
1047
|
-
};
|
|
1048
935
|
`;
|
|
1049
936
|
await fs.outputFile(
|
|
1050
937
|
path.join(dest, "src", "api", "admin", "users", `[id].${ext}`),
|
|
@@ -1084,9 +971,9 @@ export const meta: RouteMeta = {
|
|
|
1084
971
|
};
|
|
1085
972
|
|
|
1086
973
|
` : "";
|
|
1087
|
-
const profileDbImport =
|
|
1088
|
-
|
|
1089
|
-
const profileContent =
|
|
974
|
+
const profileDbImport = `import { User } from '../../model/User.js';
|
|
975
|
+
`;
|
|
976
|
+
const profileContent = ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
1090
977
|
import type { Request, Response } from 'express';
|
|
1091
978
|
${profileDbImport}${profileMeta}${middlewares}
|
|
1092
979
|
export const GET = async (req: Request, res: Response) => {
|
|
@@ -1123,29 +1010,6 @@ export const PUT = async (req, res) => {
|
|
|
1123
1010
|
const { password: _, ...safe } = updated;
|
|
1124
1011
|
res.json({ message: 'Profile updated', user: safe });
|
|
1125
1012
|
};
|
|
1126
|
-
` : ts ? `import { requireAuth } from 'express-file-cluster/auth';
|
|
1127
|
-
import type { Request, Response } from 'express';
|
|
1128
|
-
${profileMeta}${middlewares}
|
|
1129
|
-
export const GET = async (req: Request, res: Response) => {
|
|
1130
|
-
res.json({ user: (req as any).user });
|
|
1131
|
-
};
|
|
1132
|
-
|
|
1133
|
-
export const PUT = async (req: Request, res: Response) => {
|
|
1134
|
-
const { name, email } = req.body;
|
|
1135
|
-
// TODO: update user in DB
|
|
1136
|
-
res.json({ message: 'Profile updated', user: { ...(req as any).user, name, email } });
|
|
1137
|
-
};
|
|
1138
|
-
` : `import { requireAuth } from 'express-file-cluster/auth';
|
|
1139
|
-
${profileMeta}${middlewares}
|
|
1140
|
-
export const GET = async (req, res) => {
|
|
1141
|
-
res.json({ user: req.user });
|
|
1142
|
-
};
|
|
1143
|
-
|
|
1144
|
-
export const PUT = async (req, res) => {
|
|
1145
|
-
const { name, email } = req.body;
|
|
1146
|
-
// TODO: update user in DB
|
|
1147
|
-
res.json({ message: 'Profile updated', user: { ...req.user, name, email } });
|
|
1148
|
-
};
|
|
1149
1013
|
`;
|
|
1150
1014
|
await fs.outputFile(path.join(dest, "src", "api", "user", `profile.${ext}`), profileContent);
|
|
1151
1015
|
}
|
|
@@ -1181,27 +1045,7 @@ ${blocks}
|
|
|
1181
1045
|
}
|
|
1182
1046
|
async function writeSessionModel(dest, opts) {
|
|
1183
1047
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1184
|
-
const
|
|
1185
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1186
|
-
|
|
1187
|
-
export interface SessionDocument {
|
|
1188
|
-
userId: string;
|
|
1189
|
-
token: string;
|
|
1190
|
-
ip: string;
|
|
1191
|
-
userAgent: string;
|
|
1192
|
-
expiresAt: Date;
|
|
1193
|
-
isActive: boolean;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
export const Session = defineModel<SessionDocument>('Session', {
|
|
1197
|
-
userId: { type: 'string', required: true },
|
|
1198
|
-
token: { type: 'string', required: true, unique: true },
|
|
1199
|
-
ip: { type: 'string', required: true },
|
|
1200
|
-
userAgent: { type: 'string', required: true },
|
|
1201
|
-
expiresAt: { type: 'date', required: true },
|
|
1202
|
-
isActive: { type: 'boolean', default: true },
|
|
1203
|
-
});
|
|
1204
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1048
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1205
1049
|
|
|
1206
1050
|
export const Session = defineModel('Session', {
|
|
1207
1051
|
userId: { type: 'string', required: true },
|
|
@@ -1211,47 +1055,12 @@ export const Session = defineModel('Session', {
|
|
|
1211
1055
|
expiresAt: { type: 'date', required: true },
|
|
1212
1056
|
isActive: { type: 'boolean', default: true },
|
|
1213
1057
|
});
|
|
1214
|
-
` : ts ? `// TODO: define your Drizzle schema for Session
|
|
1215
|
-
// import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
1216
|
-
//
|
|
1217
|
-
// export const sessions = pgTable('sessions', {
|
|
1218
|
-
// id: serial('id').primaryKey(),
|
|
1219
|
-
// userId: text('user_id').notNull(),
|
|
1220
|
-
// token: text('token').notNull().unique(),
|
|
1221
|
-
// ip: text('ip').notNull(),
|
|
1222
|
-
// userAgent: text('user_agent').notNull(),
|
|
1223
|
-
// expiresAt: timestamp('expires_at').notNull(),
|
|
1224
|
-
// isActive: boolean('is_active').notNull().default(true),
|
|
1225
|
-
// });
|
|
1226
|
-
export {};
|
|
1227
|
-
` : `// TODO: define your Drizzle schema for Session
|
|
1228
|
-
export {};
|
|
1229
1058
|
`;
|
|
1230
1059
|
await fs.outputFile(path.join(dest, "src", "model", `Session.${ext}`), content);
|
|
1231
1060
|
}
|
|
1232
1061
|
async function writeNotificationModel(dest, opts) {
|
|
1233
1062
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1234
|
-
const
|
|
1235
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1236
|
-
|
|
1237
|
-
export interface NotificationDocument {
|
|
1238
|
-
userId: string;
|
|
1239
|
-
title: string;
|
|
1240
|
-
message: string;
|
|
1241
|
-
type: string;
|
|
1242
|
-
isRead: boolean;
|
|
1243
|
-
link?: string;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
export const Notification = defineModel<NotificationDocument>('Notification', {
|
|
1247
|
-
userId: { type: 'string', required: true },
|
|
1248
|
-
title: { type: 'string', required: true },
|
|
1249
|
-
message: { type: 'string', required: true },
|
|
1250
|
-
type: { type: 'string', required: true, default: 'info' },
|
|
1251
|
-
isRead: { type: 'boolean', default: false },
|
|
1252
|
-
link: { type: 'string' },
|
|
1253
|
-
});
|
|
1254
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1063
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1255
1064
|
|
|
1256
1065
|
export const Notification = defineModel('Notification', {
|
|
1257
1066
|
userId: { type: 'string', required: true },
|
|
@@ -1261,50 +1070,12 @@ export const Notification = defineModel('Notification', {
|
|
|
1261
1070
|
isRead: { type: 'boolean', default: false },
|
|
1262
1071
|
link: { type: 'string' },
|
|
1263
1072
|
});
|
|
1264
|
-
` : ts ? `// TODO: define your Drizzle schema for Notification
|
|
1265
|
-
// import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
1266
|
-
//
|
|
1267
|
-
// export const notifications = pgTable('notifications', {
|
|
1268
|
-
// id: serial('id').primaryKey(),
|
|
1269
|
-
// userId: text('user_id').notNull(),
|
|
1270
|
-
// title: text('title').notNull(),
|
|
1271
|
-
// message: text('message').notNull(),
|
|
1272
|
-
// type: text('type').notNull().default('info'),
|
|
1273
|
-
// isRead: boolean('is_read').notNull().default(false),
|
|
1274
|
-
// link: text('link'),
|
|
1275
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1276
|
-
// });
|
|
1277
|
-
export {};
|
|
1278
|
-
` : `// TODO: define your Drizzle schema for Notification
|
|
1279
|
-
export {};
|
|
1280
1073
|
`;
|
|
1281
1074
|
await fs.outputFile(path.join(dest, "src", "model", `Notification.${ext}`), content);
|
|
1282
1075
|
}
|
|
1283
1076
|
async function writeFileModel(dest, opts) {
|
|
1284
1077
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1285
|
-
const
|
|
1286
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1287
|
-
|
|
1288
|
-
export interface FileDocument {
|
|
1289
|
-
userId: string;
|
|
1290
|
-
filename: string;
|
|
1291
|
-
originalName: string;
|
|
1292
|
-
mimeType: string;
|
|
1293
|
-
size: number;
|
|
1294
|
-
url: string;
|
|
1295
|
-
isPublic: boolean;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
export const File = defineModel<FileDocument>('File', {
|
|
1299
|
-
userId: { type: 'string', required: true },
|
|
1300
|
-
filename: { type: 'string', required: true },
|
|
1301
|
-
originalName: { type: 'string', required: true },
|
|
1302
|
-
mimeType: { type: 'string', required: true },
|
|
1303
|
-
size: { type: 'number', required: true },
|
|
1304
|
-
url: { type: 'string', required: true },
|
|
1305
|
-
isPublic: { type: 'boolean', default: false },
|
|
1306
|
-
});
|
|
1307
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1078
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1308
1079
|
|
|
1309
1080
|
export const File = defineModel('File', {
|
|
1310
1081
|
userId: { type: 'string', required: true },
|
|
@@ -1315,49 +1086,12 @@ export const File = defineModel('File', {
|
|
|
1315
1086
|
url: { type: 'string', required: true },
|
|
1316
1087
|
isPublic: { type: 'boolean', default: false },
|
|
1317
1088
|
});
|
|
1318
|
-
` : ts ? `// TODO: define your Drizzle schema for File
|
|
1319
|
-
// import { pgTable, serial, text, boolean, integer, timestamp } from 'drizzle-orm/pg-core';
|
|
1320
|
-
//
|
|
1321
|
-
// export const files = pgTable('files', {
|
|
1322
|
-
// id: serial('id').primaryKey(),
|
|
1323
|
-
// userId: text('user_id').notNull(),
|
|
1324
|
-
// filename: text('filename').notNull(),
|
|
1325
|
-
// originalName: text('original_name').notNull(),
|
|
1326
|
-
// mimeType: text('mime_type').notNull(),
|
|
1327
|
-
// size: integer('size').notNull(),
|
|
1328
|
-
// url: text('url').notNull(),
|
|
1329
|
-
// isPublic: boolean('is_public').notNull().default(false),
|
|
1330
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1331
|
-
// });
|
|
1332
|
-
export {};
|
|
1333
|
-
` : `// TODO: define your Drizzle schema for File
|
|
1334
|
-
export {};
|
|
1335
1089
|
`;
|
|
1336
1090
|
await fs.outputFile(path.join(dest, "src", "model", `File.${ext}`), content);
|
|
1337
1091
|
}
|
|
1338
1092
|
async function writeSupportTicketModel(dest, opts) {
|
|
1339
1093
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1340
|
-
const
|
|
1341
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1342
|
-
|
|
1343
|
-
export interface SupportTicketDocument {
|
|
1344
|
-
userId: string;
|
|
1345
|
-
subject: string;
|
|
1346
|
-
message: string;
|
|
1347
|
-
status: string;
|
|
1348
|
-
priority: string;
|
|
1349
|
-
assignedTo?: string;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
export const SupportTicket = defineModel<SupportTicketDocument>('SupportTicket', {
|
|
1353
|
-
userId: { type: 'string', required: true },
|
|
1354
|
-
subject: { type: 'string', required: true },
|
|
1355
|
-
message: { type: 'string', required: true },
|
|
1356
|
-
status: { type: 'string', required: true, default: 'open' },
|
|
1357
|
-
priority: { type: 'string', required: true, default: 'normal' },
|
|
1358
|
-
assignedTo: { type: 'string' },
|
|
1359
|
-
});
|
|
1360
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1094
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1361
1095
|
|
|
1362
1096
|
export const SupportTicket = defineModel('SupportTicket', {
|
|
1363
1097
|
userId: { type: 'string', required: true },
|
|
@@ -1367,49 +1101,12 @@ export const SupportTicket = defineModel('SupportTicket', {
|
|
|
1367
1101
|
priority: { type: 'string', required: true, default: 'normal' },
|
|
1368
1102
|
assignedTo: { type: 'string' },
|
|
1369
1103
|
});
|
|
1370
|
-
` : ts ? `// TODO: define your Drizzle schema for SupportTicket
|
|
1371
|
-
// import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
|
1372
|
-
//
|
|
1373
|
-
// export const supportTickets = pgTable('support_tickets', {
|
|
1374
|
-
// id: serial('id').primaryKey(),
|
|
1375
|
-
// userId: text('user_id').notNull(),
|
|
1376
|
-
// subject: text('subject').notNull(),
|
|
1377
|
-
// message: text('message').notNull(),
|
|
1378
|
-
// status: text('status').notNull().default('open'),
|
|
1379
|
-
// priority: text('priority').notNull().default('normal'),
|
|
1380
|
-
// assignedTo: text('assigned_to'),
|
|
1381
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1382
|
-
// updatedAt: timestamp('updated_at').defaultNow(),
|
|
1383
|
-
// });
|
|
1384
|
-
export {};
|
|
1385
|
-
` : `// TODO: define your Drizzle schema for SupportTicket
|
|
1386
|
-
export {};
|
|
1387
1104
|
`;
|
|
1388
1105
|
await fs.outputFile(path.join(dest, "src", "model", `SupportTicket.${ext}`), content);
|
|
1389
1106
|
}
|
|
1390
1107
|
async function writeAuditLogModel(dest, opts) {
|
|
1391
1108
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1392
|
-
const
|
|
1393
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1394
|
-
|
|
1395
|
-
export interface AuditLogDocument {
|
|
1396
|
-
adminId: string;
|
|
1397
|
-
action: string;
|
|
1398
|
-
entity: string;
|
|
1399
|
-
entityId: string;
|
|
1400
|
-
metadata?: Record<string, unknown>;
|
|
1401
|
-
ip: string;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
export const AuditLog = defineModel<AuditLogDocument>('AuditLog', {
|
|
1405
|
-
adminId: { type: 'string', required: true },
|
|
1406
|
-
action: { type: 'string', required: true },
|
|
1407
|
-
entity: { type: 'string', required: true },
|
|
1408
|
-
entityId: { type: 'string', required: true },
|
|
1409
|
-
metadata: { type: 'object' },
|
|
1410
|
-
ip: { type: 'string', required: true },
|
|
1411
|
-
});
|
|
1412
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1109
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1413
1110
|
|
|
1414
1111
|
export const AuditLog = defineModel('AuditLog', {
|
|
1415
1112
|
adminId: { type: 'string', required: true },
|
|
@@ -1419,48 +1116,12 @@ export const AuditLog = defineModel('AuditLog', {
|
|
|
1419
1116
|
metadata: { type: 'object' },
|
|
1420
1117
|
ip: { type: 'string', required: true },
|
|
1421
1118
|
});
|
|
1422
|
-
` : ts ? `// TODO: define your Drizzle schema for AuditLog
|
|
1423
|
-
// import { pgTable, serial, text, jsonb, timestamp } from 'drizzle-orm/pg-core';
|
|
1424
|
-
//
|
|
1425
|
-
// export const auditLogs = pgTable('audit_logs', {
|
|
1426
|
-
// id: serial('id').primaryKey(),
|
|
1427
|
-
// adminId: text('admin_id').notNull(),
|
|
1428
|
-
// action: text('action').notNull(),
|
|
1429
|
-
// entity: text('entity').notNull(),
|
|
1430
|
-
// entityId: text('entity_id').notNull(),
|
|
1431
|
-
// metadata: jsonb('metadata'),
|
|
1432
|
-
// ip: text('ip').notNull(),
|
|
1433
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1434
|
-
// });
|
|
1435
|
-
export {};
|
|
1436
|
-
` : `// TODO: define your Drizzle schema for AuditLog
|
|
1437
|
-
export {};
|
|
1438
1119
|
`;
|
|
1439
1120
|
await fs.outputFile(path.join(dest, "src", "model", `AuditLog.${ext}`), content);
|
|
1440
1121
|
}
|
|
1441
1122
|
async function writeSubscriptionModel(dest, opts) {
|
|
1442
1123
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1443
|
-
const
|
|
1444
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1445
|
-
|
|
1446
|
-
export interface SubscriptionDocument {
|
|
1447
|
-
userId: string;
|
|
1448
|
-
planId: string;
|
|
1449
|
-
status: string;
|
|
1450
|
-
startDate: Date;
|
|
1451
|
-
endDate: Date;
|
|
1452
|
-
cancelledAt?: Date;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
export const Subscription = defineModel<SubscriptionDocument>('Subscription', {
|
|
1456
|
-
userId: { type: 'string', required: true },
|
|
1457
|
-
planId: { type: 'string', required: true },
|
|
1458
|
-
status: { type: 'string', required: true, default: 'active' },
|
|
1459
|
-
startDate: { type: 'date', required: true },
|
|
1460
|
-
endDate: { type: 'date', required: true },
|
|
1461
|
-
cancelledAt: { type: 'date' },
|
|
1462
|
-
});
|
|
1463
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1124
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1464
1125
|
|
|
1465
1126
|
export const Subscription = defineModel('Subscription', {
|
|
1466
1127
|
userId: { type: 'string', required: true },
|
|
@@ -1470,99 +1131,27 @@ export const Subscription = defineModel('Subscription', {
|
|
|
1470
1131
|
endDate: { type: 'date', required: true },
|
|
1471
1132
|
cancelledAt: { type: 'date' },
|
|
1472
1133
|
});
|
|
1473
|
-
` : ts ? `// TODO: define your Drizzle schema for Subscription
|
|
1474
|
-
// import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
|
1475
|
-
//
|
|
1476
|
-
// export const subscriptions = pgTable('subscriptions', {
|
|
1477
|
-
// id: serial('id').primaryKey(),
|
|
1478
|
-
// userId: text('user_id').notNull(),
|
|
1479
|
-
// planId: text('plan_id').notNull(),
|
|
1480
|
-
// status: text('status').notNull().default('active'),
|
|
1481
|
-
// startDate: timestamp('start_date').notNull(),
|
|
1482
|
-
// endDate: timestamp('end_date').notNull(),
|
|
1483
|
-
// cancelledAt: timestamp('cancelled_at'),
|
|
1484
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1485
|
-
// });
|
|
1486
|
-
export {};
|
|
1487
|
-
` : `// TODO: define your Drizzle schema for Subscription
|
|
1488
|
-
export {};
|
|
1489
1134
|
`;
|
|
1490
1135
|
await fs.outputFile(path.join(dest, "src", "model", `Subscription.${ext}`), content);
|
|
1491
1136
|
}
|
|
1492
1137
|
async function writePlanModel(dest, opts) {
|
|
1493
1138
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1494
|
-
const
|
|
1495
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1496
|
-
|
|
1497
|
-
export interface PlanDocument {
|
|
1498
|
-
name: string;
|
|
1499
|
-
description: string;
|
|
1500
|
-
price: number;
|
|
1501
|
-
interval: string;
|
|
1502
|
-
features: string[];
|
|
1503
|
-
isActive: boolean;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
export const Plan = defineModel<PlanDocument>('Plan', {
|
|
1507
|
-
name: { type: 'string', required: true },
|
|
1508
|
-
description: { type: 'string', required: true },
|
|
1509
|
-
price: { type: 'number', required: true },
|
|
1510
|
-
interval: { type: 'string', required: true, default: 'monthly' },
|
|
1511
|
-
features: { type: 'array', default: [] },
|
|
1512
|
-
isActive: { type: 'boolean', default: true },
|
|
1513
|
-
});
|
|
1514
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1139
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1515
1140
|
|
|
1516
1141
|
export const Plan = defineModel('Plan', {
|
|
1517
1142
|
name: { type: 'string', required: true },
|
|
1518
1143
|
description: { type: 'string', required: true },
|
|
1519
1144
|
price: { type: 'number', required: true },
|
|
1520
1145
|
interval: { type: 'string', required: true, default: 'monthly' },
|
|
1521
|
-
features: { type: 'array', default: [] },
|
|
1146
|
+
features: { type: 'array', of: 'string', default: [] },
|
|
1522
1147
|
isActive: { type: 'boolean', default: true },
|
|
1523
1148
|
});
|
|
1524
|
-
` : ts ? `// TODO: define your Drizzle schema for Plan
|
|
1525
|
-
// import { pgTable, serial, text, numeric, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
1526
|
-
//
|
|
1527
|
-
// export const plans = pgTable('plans', {
|
|
1528
|
-
// id: serial('id').primaryKey(),
|
|
1529
|
-
// name: text('name').notNull(),
|
|
1530
|
-
// description: text('description').notNull(),
|
|
1531
|
-
// price: numeric('price').notNull(),
|
|
1532
|
-
// interval: text('interval').notNull().default('monthly'),
|
|
1533
|
-
// features: text('features').array().notNull().default([]),
|
|
1534
|
-
// isActive: boolean('is_active').notNull().default(true),
|
|
1535
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1536
|
-
// });
|
|
1537
|
-
export {};
|
|
1538
|
-
` : `// TODO: define your Drizzle schema for Plan
|
|
1539
|
-
export {};
|
|
1540
1149
|
`;
|
|
1541
1150
|
await fs.outputFile(path.join(dest, "src", "model", `Plan.${ext}`), content);
|
|
1542
1151
|
}
|
|
1543
1152
|
async function writeInvoiceModel(dest, opts) {
|
|
1544
1153
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1545
|
-
const
|
|
1546
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1547
|
-
|
|
1548
|
-
export interface InvoiceDocument {
|
|
1549
|
-
userId: string;
|
|
1550
|
-
subscriptionId: string;
|
|
1551
|
-
amount: number;
|
|
1552
|
-
currency: string;
|
|
1553
|
-
status: string;
|
|
1554
|
-
paidAt?: Date;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
export const Invoice = defineModel<InvoiceDocument>('Invoice', {
|
|
1558
|
-
userId: { type: 'string', required: true },
|
|
1559
|
-
subscriptionId: { type: 'string', required: true },
|
|
1560
|
-
amount: { type: 'number', required: true },
|
|
1561
|
-
currency: { type: 'string', required: true, default: 'USD' },
|
|
1562
|
-
status: { type: 'string', required: true, default: 'pending' },
|
|
1563
|
-
paidAt: { type: 'date' },
|
|
1564
|
-
});
|
|
1565
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1154
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1566
1155
|
|
|
1567
1156
|
export const Invoice = defineModel('Invoice', {
|
|
1568
1157
|
userId: { type: 'string', required: true },
|
|
@@ -1572,48 +1161,12 @@ export const Invoice = defineModel('Invoice', {
|
|
|
1572
1161
|
status: { type: 'string', required: true, default: 'pending' },
|
|
1573
1162
|
paidAt: { type: 'date' },
|
|
1574
1163
|
});
|
|
1575
|
-
` : ts ? `// TODO: define your Drizzle schema for Invoice
|
|
1576
|
-
// import { pgTable, serial, text, numeric, timestamp } from 'drizzle-orm/pg-core';
|
|
1577
|
-
//
|
|
1578
|
-
// export const invoices = pgTable('invoices', {
|
|
1579
|
-
// id: serial('id').primaryKey(),
|
|
1580
|
-
// userId: text('user_id').notNull(),
|
|
1581
|
-
// subscriptionId: text('subscription_id').notNull(),
|
|
1582
|
-
// amount: numeric('amount').notNull(),
|
|
1583
|
-
// currency: text('currency').notNull().default('USD'),
|
|
1584
|
-
// status: text('status').notNull().default('pending'),
|
|
1585
|
-
// paidAt: timestamp('paid_at'),
|
|
1586
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1587
|
-
// });
|
|
1588
|
-
export {};
|
|
1589
|
-
` : `// TODO: define your Drizzle schema for Invoice
|
|
1590
|
-
export {};
|
|
1591
1164
|
`;
|
|
1592
1165
|
await fs.outputFile(path.join(dest, "src", "model", `Invoice.${ext}`), content);
|
|
1593
1166
|
}
|
|
1594
1167
|
async function writeApiKeyModel(dest, opts) {
|
|
1595
1168
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1596
|
-
const
|
|
1597
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1598
|
-
|
|
1599
|
-
export interface ApiKeyDocument {
|
|
1600
|
-
userId: string;
|
|
1601
|
-
name: string;
|
|
1602
|
-
key: string;
|
|
1603
|
-
lastUsed?: Date;
|
|
1604
|
-
expiresAt?: Date;
|
|
1605
|
-
isActive: boolean;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
export const ApiKey = defineModel<ApiKeyDocument>('ApiKey', {
|
|
1609
|
-
userId: { type: 'string', required: true },
|
|
1610
|
-
name: { type: 'string', required: true },
|
|
1611
|
-
key: { type: 'string', required: true, unique: true },
|
|
1612
|
-
lastUsed: { type: 'date' },
|
|
1613
|
-
expiresAt:{ type: 'date' },
|
|
1614
|
-
isActive: { type: 'boolean', default: true },
|
|
1615
|
-
});
|
|
1616
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1169
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1617
1170
|
|
|
1618
1171
|
export const ApiKey = defineModel('ApiKey', {
|
|
1619
1172
|
userId: { type: 'string', required: true },
|
|
@@ -1623,85 +1176,24 @@ export const ApiKey = defineModel('ApiKey', {
|
|
|
1623
1176
|
expiresAt: { type: 'date' },
|
|
1624
1177
|
isActive: { type: 'boolean', default: true },
|
|
1625
1178
|
});
|
|
1626
|
-
` : ts ? `// TODO: define your Drizzle schema for ApiKey
|
|
1627
|
-
// import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
1628
|
-
//
|
|
1629
|
-
// export const apiKeys = pgTable('api_keys', {
|
|
1630
|
-
// id: serial('id').primaryKey(),
|
|
1631
|
-
// userId: text('user_id').notNull(),
|
|
1632
|
-
// name: text('name').notNull(),
|
|
1633
|
-
// key: text('key').notNull().unique(),
|
|
1634
|
-
// lastUsed: timestamp('last_used'),
|
|
1635
|
-
// expiresAt: timestamp('expires_at'),
|
|
1636
|
-
// isActive: boolean('is_active').notNull().default(true),
|
|
1637
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1638
|
-
// });
|
|
1639
|
-
export {};
|
|
1640
|
-
` : `// TODO: define your Drizzle schema for ApiKey
|
|
1641
|
-
export {};
|
|
1642
1179
|
`;
|
|
1643
1180
|
await fs.outputFile(path.join(dest, "src", "model", `ApiKey.${ext}`), content);
|
|
1644
1181
|
}
|
|
1645
1182
|
async function writeRoleModel(dest, opts) {
|
|
1646
1183
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1647
|
-
const
|
|
1648
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1649
|
-
|
|
1650
|
-
export interface RoleDocument {
|
|
1651
|
-
name: string;
|
|
1652
|
-
description: string;
|
|
1653
|
-
permissions: string[];
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
export const Role = defineModel<RoleDocument>('Role', {
|
|
1657
|
-
name: { type: 'string', required: true, unique: true },
|
|
1658
|
-
description: { type: 'string', required: true },
|
|
1659
|
-
permissions: { type: 'array', default: [] },
|
|
1660
|
-
});
|
|
1661
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1184
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1662
1185
|
|
|
1663
1186
|
export const Role = defineModel('Role', {
|
|
1664
1187
|
name: { type: 'string', required: true, unique: true },
|
|
1665
1188
|
description: { type: 'string', required: true },
|
|
1666
|
-
permissions: { type: 'array', default: [] },
|
|
1189
|
+
permissions: { type: 'array', of: 'string', default: [] },
|
|
1667
1190
|
});
|
|
1668
|
-
` : ts ? `// TODO: define your Drizzle schema for Role
|
|
1669
|
-
// import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
|
1670
|
-
//
|
|
1671
|
-
// export const roles = pgTable('roles', {
|
|
1672
|
-
// id: serial('id').primaryKey(),
|
|
1673
|
-
// name: text('name').notNull().unique(),
|
|
1674
|
-
// description: text('description').notNull(),
|
|
1675
|
-
// permissions: text('permissions').array().notNull().default([]),
|
|
1676
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1677
|
-
// });
|
|
1678
|
-
export {};
|
|
1679
|
-
` : `// TODO: define your Drizzle schema for Role
|
|
1680
|
-
export {};
|
|
1681
1191
|
`;
|
|
1682
1192
|
await fs.outputFile(path.join(dest, "src", "model", `Role.${ext}`), content);
|
|
1683
1193
|
}
|
|
1684
1194
|
async function writeFAQModel(dest, opts) {
|
|
1685
1195
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1686
|
-
const
|
|
1687
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1688
|
-
|
|
1689
|
-
export interface FAQDocument {
|
|
1690
|
-
question: string;
|
|
1691
|
-
answer: string;
|
|
1692
|
-
category: string;
|
|
1693
|
-
order: number;
|
|
1694
|
-
isPublished: boolean;
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
export const FAQ = defineModel<FAQDocument>('FAQ', {
|
|
1698
|
-
question: { type: 'string', required: true },
|
|
1699
|
-
answer: { type: 'string', required: true },
|
|
1700
|
-
category: { type: 'string', required: true, default: 'general' },
|
|
1701
|
-
order: { type: 'number', default: 0 },
|
|
1702
|
-
isPublished: { type: 'boolean', default: false },
|
|
1703
|
-
});
|
|
1704
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1196
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1705
1197
|
|
|
1706
1198
|
export const FAQ = defineModel('FAQ', {
|
|
1707
1199
|
question: { type: 'string', required: true },
|
|
@@ -1710,51 +1202,12 @@ export const FAQ = defineModel('FAQ', {
|
|
|
1710
1202
|
order: { type: 'number', default: 0 },
|
|
1711
1203
|
isPublished: { type: 'boolean', default: false },
|
|
1712
1204
|
});
|
|
1713
|
-
` : ts ? `// TODO: define your Drizzle schema for FAQ
|
|
1714
|
-
// import { pgTable, serial, text, boolean, integer, timestamp } from 'drizzle-orm/pg-core';
|
|
1715
|
-
//
|
|
1716
|
-
// export const faqs = pgTable('faqs', {
|
|
1717
|
-
// id: serial('id').primaryKey(),
|
|
1718
|
-
// question: text('question').notNull(),
|
|
1719
|
-
// answer: text('answer').notNull(),
|
|
1720
|
-
// category: text('category').notNull().default('general'),
|
|
1721
|
-
// order: integer('order').notNull().default(0),
|
|
1722
|
-
// isPublished: boolean('is_published').notNull().default(false),
|
|
1723
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1724
|
-
// });
|
|
1725
|
-
export {};
|
|
1726
|
-
` : `// TODO: define your Drizzle schema for FAQ
|
|
1727
|
-
export {};
|
|
1728
1205
|
`;
|
|
1729
1206
|
await fs.outputFile(path.join(dest, "src", "model", `FAQ.${ext}`), content);
|
|
1730
1207
|
}
|
|
1731
1208
|
async function writeBlogModel(dest, opts) {
|
|
1732
1209
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1733
|
-
const
|
|
1734
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1735
|
-
|
|
1736
|
-
export interface BlogDocument {
|
|
1737
|
-
title: string;
|
|
1738
|
-
slug: string;
|
|
1739
|
-
content: string;
|
|
1740
|
-
authorId: string;
|
|
1741
|
-
category: string;
|
|
1742
|
-
tags: string[];
|
|
1743
|
-
status: string;
|
|
1744
|
-
publishedAt?: Date;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
export const Blog = defineModel<BlogDocument>('Blog', {
|
|
1748
|
-
title: { type: 'string', required: true },
|
|
1749
|
-
slug: { type: 'string', required: true, unique: true },
|
|
1750
|
-
content: { type: 'string', required: true },
|
|
1751
|
-
authorId: { type: 'string', required: true },
|
|
1752
|
-
category: { type: 'string', required: true, default: 'general' },
|
|
1753
|
-
tags: { type: 'array', default: [] },
|
|
1754
|
-
status: { type: 'string', required: true, default: 'draft' },
|
|
1755
|
-
publishedAt: { type: 'date' },
|
|
1756
|
-
});
|
|
1757
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1210
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1758
1211
|
|
|
1759
1212
|
export const Blog = defineModel('Blog', {
|
|
1760
1213
|
title: { type: 'string', required: true },
|
|
@@ -1762,51 +1215,16 @@ export const Blog = defineModel('Blog', {
|
|
|
1762
1215
|
content: { type: 'string', required: true },
|
|
1763
1216
|
authorId: { type: 'string', required: true },
|
|
1764
1217
|
category: { type: 'string', required: true, default: 'general' },
|
|
1765
|
-
tags: { type: 'array', default: [] },
|
|
1218
|
+
tags: { type: 'array', of: 'string', default: [] },
|
|
1766
1219
|
status: { type: 'string', required: true, default: 'draft' },
|
|
1767
1220
|
publishedAt: { type: 'date' },
|
|
1768
1221
|
});
|
|
1769
|
-
` : ts ? `// TODO: define your Drizzle schema for Blog
|
|
1770
|
-
// import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
|
1771
|
-
//
|
|
1772
|
-
// export const blogs = pgTable('blogs', {
|
|
1773
|
-
// id: serial('id').primaryKey(),
|
|
1774
|
-
// title: text('title').notNull(),
|
|
1775
|
-
// slug: text('slug').notNull().unique(),
|
|
1776
|
-
// content: text('content').notNull(),
|
|
1777
|
-
// authorId: text('author_id').notNull(),
|
|
1778
|
-
// category: text('category').notNull().default('general'),
|
|
1779
|
-
// tags: text('tags').array().notNull().default([]),
|
|
1780
|
-
// status: text('status').notNull().default('draft'),
|
|
1781
|
-
// publishedAt: timestamp('published_at'),
|
|
1782
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1783
|
-
// updatedAt: timestamp('updated_at').defaultNow(),
|
|
1784
|
-
// });
|
|
1785
|
-
export {};
|
|
1786
|
-
` : `// TODO: define your Drizzle schema for Blog
|
|
1787
|
-
export {};
|
|
1788
1222
|
`;
|
|
1789
1223
|
await fs.outputFile(path.join(dest, "src", "model", `Blog.${ext}`), content);
|
|
1790
1224
|
}
|
|
1791
1225
|
async function writeCategoryModel(dest, opts) {
|
|
1792
1226
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1793
|
-
const
|
|
1794
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1795
|
-
|
|
1796
|
-
export interface CategoryDocument {
|
|
1797
|
-
name: string;
|
|
1798
|
-
slug: string;
|
|
1799
|
-
description?: string;
|
|
1800
|
-
parentId?: string;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
export const Category = defineModel<CategoryDocument>('Category', {
|
|
1804
|
-
name: { type: 'string', required: true },
|
|
1805
|
-
slug: { type: 'string', required: true, unique: true },
|
|
1806
|
-
description: { type: 'string' },
|
|
1807
|
-
parentId: { type: 'string' },
|
|
1808
|
-
});
|
|
1809
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1227
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1810
1228
|
|
|
1811
1229
|
export const Category = defineModel('Category', {
|
|
1812
1230
|
name: { type: 'string', required: true },
|
|
@@ -1814,48 +1232,12 @@ export const Category = defineModel('Category', {
|
|
|
1814
1232
|
description: { type: 'string' },
|
|
1815
1233
|
parentId: { type: 'string' },
|
|
1816
1234
|
});
|
|
1817
|
-
` : ts ? `// TODO: define your Drizzle schema for Category
|
|
1818
|
-
// import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
|
1819
|
-
//
|
|
1820
|
-
// export const categories = pgTable('categories', {
|
|
1821
|
-
// id: serial('id').primaryKey(),
|
|
1822
|
-
// name: text('name').notNull(),
|
|
1823
|
-
// slug: text('slug').notNull().unique(),
|
|
1824
|
-
// description: text('description'),
|
|
1825
|
-
// parentId: text('parent_id'),
|
|
1826
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1827
|
-
// });
|
|
1828
|
-
export {};
|
|
1829
|
-
` : `// TODO: define your Drizzle schema for Category
|
|
1830
|
-
export {};
|
|
1831
1235
|
`;
|
|
1832
1236
|
await fs.outputFile(path.join(dest, "src", "model", `Category.${ext}`), content);
|
|
1833
1237
|
}
|
|
1834
1238
|
async function writeCouponModel(dest, opts) {
|
|
1835
1239
|
const ext = opts.language === "typescript" ? "ts" : "js";
|
|
1836
|
-
const
|
|
1837
|
-
const content = opts.database === "mongodb" ? ts ? `import { defineModel } from 'express-file-cluster';
|
|
1838
|
-
|
|
1839
|
-
export interface CouponDocument {
|
|
1840
|
-
code: string;
|
|
1841
|
-
type: string;
|
|
1842
|
-
value: number;
|
|
1843
|
-
maxUses: number;
|
|
1844
|
-
usedCount: number;
|
|
1845
|
-
expiresAt?: Date;
|
|
1846
|
-
isActive: boolean;
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
export const Coupon = defineModel<CouponDocument>('Coupon', {
|
|
1850
|
-
code: { type: 'string', required: true, unique: true },
|
|
1851
|
-
type: { type: 'string', required: true, default: 'percent' },
|
|
1852
|
-
value: { type: 'number', required: true },
|
|
1853
|
-
maxUses: { type: 'number', default: 0 },
|
|
1854
|
-
usedCount: { type: 'number', default: 0 },
|
|
1855
|
-
expiresAt: { type: 'date' },
|
|
1856
|
-
isActive: { type: 'boolean', default: true },
|
|
1857
|
-
});
|
|
1858
|
-
` : `import { defineModel } from 'express-file-cluster';
|
|
1240
|
+
const content = `import { defineModel } from 'express-file-cluster';
|
|
1859
1241
|
|
|
1860
1242
|
export const Coupon = defineModel('Coupon', {
|
|
1861
1243
|
code: { type: 'string', required: true, unique: true },
|
|
@@ -1866,23 +1248,6 @@ export const Coupon = defineModel('Coupon', {
|
|
|
1866
1248
|
expiresAt: { type: 'date' },
|
|
1867
1249
|
isActive: { type: 'boolean', default: true },
|
|
1868
1250
|
});
|
|
1869
|
-
` : ts ? `// TODO: define your Drizzle schema for Coupon
|
|
1870
|
-
// import { pgTable, serial, text, numeric, integer, boolean, timestamp } from 'drizzle-orm/pg-core';
|
|
1871
|
-
//
|
|
1872
|
-
// export const coupons = pgTable('coupons', {
|
|
1873
|
-
// id: serial('id').primaryKey(),
|
|
1874
|
-
// code: text('code').notNull().unique(),
|
|
1875
|
-
// type: text('type').notNull().default('percent'),
|
|
1876
|
-
// value: numeric('value').notNull(),
|
|
1877
|
-
// maxUses: integer('max_uses').notNull().default(0),
|
|
1878
|
-
// usedCount: integer('used_count').notNull().default(0),
|
|
1879
|
-
// expiresAt: timestamp('expires_at'),
|
|
1880
|
-
// isActive: boolean('is_active').notNull().default(true),
|
|
1881
|
-
// createdAt: timestamp('created_at').defaultNow(),
|
|
1882
|
-
// });
|
|
1883
|
-
export {};
|
|
1884
|
-
` : `// TODO: define your Drizzle schema for Coupon
|
|
1885
|
-
export {};
|
|
1886
1251
|
`;
|
|
1887
1252
|
await fs.outputFile(path.join(dest, "src", "model", `Coupon.${ext}`), content);
|
|
1888
1253
|
}
|
|
@@ -1895,7 +1260,6 @@ async function writeAuthExtendedRoutes(dest, opts) {
|
|
|
1895
1260
|
const mwUser3 = mwUser2;
|
|
1896
1261
|
const RA = `import { requireAuth } from 'express-file-cluster/auth';
|
|
1897
1262
|
`;
|
|
1898
|
-
const mongo = opts.database === "mongodb";
|
|
1899
1263
|
const hasAdmin = opts.adminPortal;
|
|
1900
1264
|
const uf = opts.userFeatures;
|
|
1901
1265
|
const sendResetEmail = opts.mailer ? ` var appUrl = process.env.APP_URL || 'http://localhost:3000';
|
|
@@ -1932,7 +1296,7 @@ ${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
|
1932
1296
|
const account = user;`;
|
|
1933
1297
|
const refreshPersist = hasAdmin ? ` if (user) await User.update(user.id, { refreshToken: newRefreshToken, refreshTokenExpiry });
|
|
1934
1298
|
else if (admin) await Admin.update(admin.id, { refreshToken: newRefreshToken, refreshTokenExpiry });` : ` await User.update(user.id, { refreshToken: newRefreshToken, refreshTokenExpiry });`;
|
|
1935
|
-
const refreshContent =
|
|
1299
|
+
const refreshContent = ts ? `import { issueToken } from 'express-file-cluster/auth';
|
|
1936
1300
|
import type { Request, Response } from 'express';
|
|
1937
1301
|
import crypto from 'node:crypto';
|
|
1938
1302
|
${refreshModelImports}${refreshMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
@@ -1989,15 +1353,6 @@ ${refreshPersist}
|
|
|
1989
1353
|
await issueToken(res, { id: account.id, role: account.role, email: account.email });
|
|
1990
1354
|
res.json({ message: 'Token refreshed' });
|
|
1991
1355
|
};
|
|
1992
|
-
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
1993
|
-
${refreshMeta}export const POST = async (_req: Request, res: Response) => {
|
|
1994
|
-
// TODO: validate refresh token, issue new JWT
|
|
1995
|
-
res.json({ message: 'Token refreshed' });
|
|
1996
|
-
};
|
|
1997
|
-
` : `${RA}${refreshMeta}export const POST = async (_req, res) => {
|
|
1998
|
-
// TODO: validate refresh token, issue new JWT
|
|
1999
|
-
res.json({ message: 'Token refreshed' });
|
|
2000
|
-
};
|
|
2001
1356
|
`;
|
|
2002
1357
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `refresh.${ext}`), refreshContent);
|
|
2003
1358
|
}
|
|
@@ -2014,7 +1369,7 @@ ${refreshMeta}export const POST = async (_req: Request, res: Response) => {
|
|
|
2014
1369
|
response: `{ message: 'Verification email sent' }`
|
|
2015
1370
|
}
|
|
2016
1371
|
});
|
|
2017
|
-
const veContent =
|
|
1372
|
+
const veContent = ts ? `import type { Request, Response } from 'express';
|
|
2018
1373
|
import crypto from 'node:crypto';
|
|
2019
1374
|
${mailerTaskImport}import { User } from '../../model/User.js';
|
|
2020
1375
|
${veMeta}export const GET = async (req: Request, res: Response) => {
|
|
@@ -2065,27 +1420,6 @@ ${sendVerifyEmailResend} }
|
|
|
2065
1420
|
|
|
2066
1421
|
res.json({ message: 'Verification email sent' });
|
|
2067
1422
|
};
|
|
2068
|
-
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
2069
|
-
${veMeta}export const GET = async (req: Request, res: Response) => {
|
|
2070
|
-
const { token } = req.query;
|
|
2071
|
-
// TODO: verify token and mark user as verified
|
|
2072
|
-
res.json({ message: 'Email verified' });
|
|
2073
|
-
};
|
|
2074
|
-
|
|
2075
|
-
export const POST = async (_req: Request, res: Response) => {
|
|
2076
|
-
// TODO: resend verification email
|
|
2077
|
-
res.json({ message: 'Verification email sent' });
|
|
2078
|
-
};
|
|
2079
|
-
` : `${RA}${veMeta}export const GET = async (req, res) => {
|
|
2080
|
-
const { token } = req.query;
|
|
2081
|
-
// TODO: verify token and mark user as verified
|
|
2082
|
-
res.json({ message: 'Email verified' });
|
|
2083
|
-
};
|
|
2084
|
-
|
|
2085
|
-
export const POST = async (_req, res) => {
|
|
2086
|
-
// TODO: resend verification email
|
|
2087
|
-
res.json({ message: 'Verification email sent' });
|
|
2088
|
-
};
|
|
2089
1423
|
`;
|
|
2090
1424
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `verify-email.${ext}`), veContent);
|
|
2091
1425
|
}
|
|
@@ -2105,7 +1439,7 @@ ${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
|
2105
1439
|
const fpGuard = hasAdmin ? "user || admin" : "user";
|
|
2106
1440
|
const fpPersist = hasAdmin ? ` if (user) await User.update(user.id, { resetToken, resetTokenExpiry });
|
|
2107
1441
|
else if (admin) await Admin.update(admin.id, { resetToken, resetTokenExpiry });` : ` await User.update(user.id, { resetToken, resetTokenExpiry });`;
|
|
2108
|
-
const fpContent =
|
|
1442
|
+
const fpContent = ts ? `import type { Request, Response } from 'express';
|
|
2109
1443
|
import crypto from 'node:crypto';
|
|
2110
1444
|
${mailerTaskImport}${fpModelImports}${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
2111
1445
|
|
|
@@ -2144,19 +1478,6 @@ ${sendResetEmail} }
|
|
|
2144
1478
|
|
|
2145
1479
|
res.json({ message: 'Reset email sent' });
|
|
2146
1480
|
};
|
|
2147
|
-
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
2148
|
-
${fpMeta}export const POST = async (req: Request, res: Response) => {
|
|
2149
|
-
const { email } = req.body;
|
|
2150
|
-
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
2151
|
-
// TODO: generate reset token and send email
|
|
2152
|
-
res.json({ message: 'Reset email sent' });
|
|
2153
|
-
};
|
|
2154
|
-
` : `${RA}${fpMeta}export const POST = async (req, res) => {
|
|
2155
|
-
const { email } = req.body;
|
|
2156
|
-
if (!email) return res.status(400).json({ error: 'email is required' });
|
|
2157
|
-
// TODO: generate reset token and send email
|
|
2158
|
-
res.json({ message: 'Reset email sent' });
|
|
2159
|
-
};
|
|
2160
1481
|
`;
|
|
2161
1482
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `forgot-password.${ext}`), fpContent);
|
|
2162
1483
|
const rpMeta = mkMeta(opts, {
|
|
@@ -2175,8 +1496,9 @@ ${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
|
2175
1496
|
const account = user;`;
|
|
2176
1497
|
const rpPersist = hasAdmin ? ` if (user) await User.update(user.id, { password: hashed, resetToken: '' });
|
|
2177
1498
|
else if (admin) await Admin.update(admin.id, { password: hashed, resetToken: '' });` : ` await User.update(user.id, { password: hashed, resetToken: '' });`;
|
|
2178
|
-
const rpContent =
|
|
1499
|
+
const rpContent = ts ? `import type { Request, Response } from 'express';
|
|
2179
1500
|
import bcrypt from 'bcrypt';
|
|
1501
|
+
import { revokeAllSessions } from 'express-file-cluster/auth';
|
|
2180
1502
|
${rpModelImports}${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
2181
1503
|
const { token, password } = req.body;
|
|
2182
1504
|
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
@@ -2189,10 +1511,13 @@ ${rpLookup}
|
|
|
2189
1511
|
|
|
2190
1512
|
const hashed = await bcrypt.hash(password, 10);
|
|
2191
1513
|
${rpPersist}
|
|
1514
|
+
// Tokens issued with the old password stop working everywhere.
|
|
1515
|
+
await revokeAllSessions(account.id);
|
|
2192
1516
|
|
|
2193
1517
|
res.json({ message: 'Password reset successfully' });
|
|
2194
1518
|
};
|
|
2195
1519
|
` : `import bcrypt from 'bcrypt';
|
|
1520
|
+
import { revokeAllSessions } from 'express-file-cluster/auth';
|
|
2196
1521
|
${rpModelImports}${rpMeta}export const POST = async (req, res) => {
|
|
2197
1522
|
const { token, password } = req.body;
|
|
2198
1523
|
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
@@ -2205,22 +1530,11 @@ ${rpLookup}
|
|
|
2205
1530
|
|
|
2206
1531
|
const hashed = await bcrypt.hash(password, 10);
|
|
2207
1532
|
${rpPersist}
|
|
1533
|
+
// Tokens issued with the old password stop working everywhere.
|
|
1534
|
+
await revokeAllSessions(account.id);
|
|
2208
1535
|
|
|
2209
1536
|
res.json({ message: 'Password reset successfully' });
|
|
2210
1537
|
};
|
|
2211
|
-
` : ts ? `${RA}import type { Request, Response } from 'express';
|
|
2212
|
-
${rpMeta}export const POST = async (req: Request, res: Response) => {
|
|
2213
|
-
const { token, password } = req.body;
|
|
2214
|
-
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
2215
|
-
// TODO: validate token, hash and update password
|
|
2216
|
-
res.json({ message: 'Password reset successfully' });
|
|
2217
|
-
};
|
|
2218
|
-
` : `${RA}${rpMeta}export const POST = async (req, res) => {
|
|
2219
|
-
const { token, password } = req.body;
|
|
2220
|
-
if (!token || !password) return res.status(400).json({ error: 'token and password are required' });
|
|
2221
|
-
// TODO: validate token, hash and update password
|
|
2222
|
-
res.json({ message: 'Password reset successfully' });
|
|
2223
|
-
};
|
|
2224
1538
|
`;
|
|
2225
1539
|
await fs.outputFile(path.join(dest, "src", "api", "auth", `reset-password.${ext}`), rpContent);
|
|
2226
1540
|
}
|
|
@@ -2237,14 +1551,16 @@ ${cpMeta}${mwUser2}
|
|
|
2237
1551
|
export const POST = async (req: Request, res: Response) => {
|
|
2238
1552
|
const { oldPassword, newPassword } = req.body;
|
|
2239
1553
|
if (!oldPassword || !newPassword) return res.status(400).json({ error: 'oldPassword and newPassword are required' });
|
|
2240
|
-
// TODO: verify old password, hash and update new password
|
|
1554
|
+
// TODO: verify old password, hash and update new password, then sign out other devices:
|
|
1555
|
+
// await revokeAllSessions(req.user.id) (import from 'express-file-cluster/auth')
|
|
2241
1556
|
res.json({ message: 'Password changed successfully' });
|
|
2242
1557
|
};
|
|
2243
1558
|
` : `${RA}${cpMeta}${mwUser2}
|
|
2244
1559
|
export const POST = async (req, res) => {
|
|
2245
1560
|
const { oldPassword, newPassword } = req.body;
|
|
2246
1561
|
if (!oldPassword || !newPassword) return res.status(400).json({ error: 'oldPassword and newPassword are required' });
|
|
2247
|
-
// TODO: verify old password, hash and update new password
|
|
1562
|
+
// TODO: verify old password, hash and update new password, then sign out other devices:
|
|
1563
|
+
// await revokeAllSessions(req.user.id) (import from 'express-file-cluster/auth')
|
|
2248
1564
|
res.json({ message: 'Password changed successfully' });
|
|
2249
1565
|
};
|
|
2250
1566
|
`;
|
|
@@ -4131,6 +3447,30 @@ ${roleGuard} const { status, page = 1, limit = 20 } = req.query;
|
|
|
4131
3447
|
);
|
|
4132
3448
|
}
|
|
4133
3449
|
}
|
|
3450
|
+
async function writeInitMigration(dest, opts) {
|
|
3451
|
+
const modelDir = path.join(dest, "src", "model");
|
|
3452
|
+
if (!await fs.pathExists(modelDir)) return;
|
|
3453
|
+
const ts = opts.language === "typescript";
|
|
3454
|
+
const ext = ts ? "ts" : "js";
|
|
3455
|
+
const names = (await fs.readdir(modelDir)).filter((f) => f.endsWith(`.${ext}`)).map((f) => f.slice(0, -(ext.length + 1))).sort();
|
|
3456
|
+
if (names.length === 0) return;
|
|
3457
|
+
const imports = names.map((n) => `import { ${n} } from '../model/${n}.js';`).join("\n");
|
|
3458
|
+
const typeImport = ts ? "import type { MigrationContext } from 'express-file-cluster';\n" : "";
|
|
3459
|
+
const sig = ts ? "(_ctx: MigrationContext): Promise<void>" : "()";
|
|
3460
|
+
const content = `${typeImport}${imports}
|
|
3461
|
+
|
|
3462
|
+
const models = [${names.join(", ")}];
|
|
3463
|
+
|
|
3464
|
+
export async function up${sig} {
|
|
3465
|
+
for (const model of models) await model.sync();
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
export async function down${sig} {
|
|
3469
|
+
for (const model of [...models].reverse()) await model.drop();
|
|
3470
|
+
}
|
|
3471
|
+
`;
|
|
3472
|
+
await fs.outputFile(path.join(dest, "src", "migrations", `00000000000000_init.${ext}`), content);
|
|
3473
|
+
}
|
|
4134
3474
|
|
|
4135
3475
|
// src/index.ts
|
|
4136
3476
|
function cancel2(msg = "Cancelled") {
|
|
@@ -4255,7 +3595,11 @@ async function main() {
|
|
|
4255
3595
|
message: "Task queue backend:",
|
|
4256
3596
|
options: [
|
|
4257
3597
|
{ value: "bullmq", label: "BullMQ", hint: "Redis" },
|
|
4258
|
-
{
|
|
3598
|
+
{
|
|
3599
|
+
value: "pg-boss",
|
|
3600
|
+
label: "pg-boss",
|
|
3601
|
+
hint: database === "postgresql" ? "uses your PostgreSQL \u2014 no Redis" : "needs PostgreSQL"
|
|
3602
|
+
}
|
|
4259
3603
|
]
|
|
4260
3604
|
});
|
|
4261
3605
|
if (p.isCancel(backend)) cancel2();
|