create-efc-app 0.9.6 → 0.10.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 +90 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -46,18 +46,22 @@ function sendsMail(opts) {
|
|
|
46
46
|
function usesTables(opts) {
|
|
47
47
|
return opts.database === "postgresql" && opts.modelApi !== "defineModel";
|
|
48
48
|
}
|
|
49
|
+
function usesCollections(opts) {
|
|
50
|
+
return opts.database === "mongodb" && opts.modelApi !== "defineModel";
|
|
51
|
+
}
|
|
49
52
|
function orm(opts) {
|
|
50
53
|
const t = usesTables(opts);
|
|
54
|
+
const c = usesCollections(opts);
|
|
51
55
|
const where = (filter) => filter.replace(/\s/g, "") === "{}" ? "" : `{ where: ${filter} }`;
|
|
52
56
|
const byId = (id) => id === "id" ? "{ id }" : `{ id: ${id} }`;
|
|
53
57
|
return {
|
|
54
58
|
findOne: (m, filter) => t ? `${m}.findFirst({ where: ${filter} })` : `${m}.findOne(${filter})`,
|
|
55
59
|
findById: (m, id) => t ? `${m}.findUnique({ where: ${byId(id)} })` : `${m}.findById(${id})`,
|
|
56
60
|
find: (m, filter) => t ? `${m}.findMany(${where(filter)})` : `${m}.find(${filter})`,
|
|
57
|
-
count: (m, filter) => t ? `${m}.count(${where(filter)})` : `${m}.count(${filter})`,
|
|
61
|
+
count: (m, filter) => t ? `${m}.count(${where(filter)})` : c ? `${m}.countDocuments(${filter})` : `${m}.count(${filter})`,
|
|
58
62
|
create: (m, data) => t ? `${m}.create({ data: ${data} })` : `${m}.create(${data})`,
|
|
59
|
-
update: (m, id, data) => t ? `${m}.update({ where: ${byId(id)}, data: ${data} })` : `${m}.update(${id}, ${data})`,
|
|
60
|
-
delete: (m, id) => t ? `${m}.delete({ where: ${byId(id)} })` : `${m}.delete(${id})`
|
|
63
|
+
update: (m, id, data) => t ? `${m}.update({ where: ${byId(id)}, data: ${data} })` : c ? `${m}.findByIdAndUpdate(${id}, ${data}, { returnDocument: 'after' })` : `${m}.update(${id}, ${data})`,
|
|
64
|
+
delete: (m, id) => t ? `${m}.delete({ where: ${byId(id)} })` : c ? `${m}.findByIdAndDelete(${id})` : `${m}.delete(${id})`
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
function generatedModels(opts) {
|
|
@@ -83,7 +87,7 @@ function generatedModels(opts) {
|
|
|
83
87
|
return m;
|
|
84
88
|
}
|
|
85
89
|
function modelApi(opts) {
|
|
86
|
-
const name = usesTables(opts) ? "defineTable" : "defineModel";
|
|
90
|
+
const name = usesTables(opts) ? "defineTable" : usesCollections(opts) ? "defineCollection" : "defineModel";
|
|
87
91
|
return { name, import: `import { ${name} } from 'express-file-cluster';` };
|
|
88
92
|
}
|
|
89
93
|
function refField(opts, r) {
|
|
@@ -153,7 +157,7 @@ async function scaffold(opts) {
|
|
|
153
157
|
}
|
|
154
158
|
async function writePackageJson(dest, opts) {
|
|
155
159
|
const deps = {
|
|
156
|
-
"express-file-cluster": "^0.
|
|
160
|
+
"express-file-cluster": "^0.10.0"
|
|
157
161
|
};
|
|
158
162
|
if (opts.database === "mongodb") deps["mongoose"] = "^9.7.2";
|
|
159
163
|
if (opts.database === "postgresql") {
|
|
@@ -238,6 +242,9 @@ ${declaration}
|
|
|
238
242
|
jwtExpiresIn: process.env.JWT_EXPIRES_IN,
|
|
239
243
|
cookieDomain: process.env.COOKIE_DOMAIN,
|
|
240
244
|
cors: corsOrigins ? { origin: corsOrigins } : true,
|
|
245
|
+
// Behind a proxy / load balancer (Render, Fly, Heroku, nginx): the number of hops, e.g. 1,
|
|
246
|
+
// so req.ip is the client and rate limits count per client.
|
|
247
|
+
trustProxy: process.env.TRUST_PROXY ? Number(process.env.TRUST_PROXY) : undefined,
|
|
241
248
|
authStrategy: '${opts.authStrategy}',
|
|
242
249
|
tasks: ${tasks},
|
|
243
250
|
// Cache + session-revocation store (logout really invalidates tokens). Redis when
|
|
@@ -311,7 +318,8 @@ Optional \u2014 the same docs over MCP, with search and scaffolding tools:
|
|
|
311
318
|
${usesTables(opts) ? `- \`src/model/*.${ext}\` \u2014 \`defineTable\` models (PostgreSQL-native, Prisma-style queries:
|
|
312
319
|
\`findMany({ where, include })\`, \`create({ data })\`, \`update({ where, data })\`). A \`ref\` is a
|
|
313
320
|
foreign key; ${opts.registry ? "add back-relations to a model's `relations` when you need `include`" : "back-relations are in each model's `relations`"}. \`src/model/index.${ext}\` imports
|
|
314
|
-
them all${opts.language === "typescript" ? " and declares `Tables` (typed `include`)" : ""} \u2014 add new models there.` : `- \`src/model/*.${ext}\` \u2014 \`
|
|
321
|
+
them all${opts.language === "typescript" ? " and declares `Tables` (typed `include`)" : ""} \u2014 add new models there.` : usesCollections(opts) ? `- \`src/model/*.${ext}\` \u2014 \`defineCollection\` models (MongoDB-native, mongoose-style queries:
|
|
322
|
+
\`find(filter, { projection, sort })\`, \`findByIdAndUpdate\`, typed \`pipeline()\`, \`lookup()\`).` : `- \`src/model/*.${ext}\` \u2014 \`defineModel\` models.`}
|
|
315
323
|
- \`src/index.${ext}\` is the entry point; it imports \`efc.config.${ext}\` and passes it to \`ignite()\`.
|
|
316
324
|
- Only three import paths exist: \`express-file-cluster\`, \`express-file-cluster/auth\`,
|
|
317
325
|
\`express-file-cluster/tasks\`.
|
|
@@ -397,7 +405,9 @@ CORS_ORIGINS=http://localhost:3000${smtpVars}${oauthEnv}`;
|
|
|
397
405
|
NODE_ENV=development
|
|
398
406
|
DATABASE_URL=${dbExampleUrl}
|
|
399
407
|
JWT_SECRET=<generate with: openssl rand -hex 64>${redis}
|
|
400
|
-
CORS_ORIGINS=http://localhost:3000,https://yourapp.com
|
|
408
|
+
CORS_ORIGINS=http://localhost:3000,https://yourapp.com
|
|
409
|
+
# Behind a proxy / load balancer: how many hops to trust, so rate limits see client IPs
|
|
410
|
+
# TRUST_PROXY=1${smtpExample}${oauthExample}`;
|
|
401
411
|
await fs.outputFile(path.join(dest, ".env"), dotenv);
|
|
402
412
|
await fs.outputFile(path.join(dest, ".env.example"), example);
|
|
403
413
|
}
|
|
@@ -624,11 +634,23 @@ ${hasUser ? `import { User } from '../../model/User.js';
|
|
|
624
634
|
res.json({ message: 'Logged in', ...(token ? { token, refreshToken } : {}) });`;
|
|
625
635
|
const loginContent = hasModels ? ts ? `import { issueToken, verifyPassword } from 'express-file-cluster/auth';
|
|
626
636
|
import type { Request, Response } from 'express';
|
|
627
|
-
|
|
637
|
+
import { rateLimit } from 'express-file-cluster';
|
|
638
|
+
${loginDbImports}${loginMeta}// 10 attempts per 15 minutes per address and account. Keyed on the email too, so behind a
|
|
639
|
+
// proxy without TRUST_PROXY (every client then has the proxy's IP) it still limits one
|
|
640
|
+
// account at a time, never everyone.
|
|
641
|
+
export const middlewares = [
|
|
642
|
+
rateLimit({
|
|
643
|
+
window: '15m',
|
|
644
|
+
max: 10,
|
|
645
|
+
key: (req) => \`\${req.ip}:\${String(req.body?.email ?? '').toLowerCase()}\`,
|
|
646
|
+
}),
|
|
647
|
+
];
|
|
648
|
+
|
|
649
|
+
const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
628
650
|
|
|
629
651
|
async function issueRefreshToken(
|
|
630
652
|
res: Response,
|
|
631
|
-
model: ${usesTables(opts) ? "{ update: (args: { where: { id: string }; data: Record<string, unknown> }) => Promise<unknown> }" : "{ update: (id: string, data: Record<string, unknown>) => Promise<unknown> }"},
|
|
653
|
+
model: ${usesTables(opts) ? "{ update: (args: { where: { id: string }; data: Record<string, unknown> }) => Promise<unknown> }" : usesCollections(opts) ? "{ findByIdAndUpdate: (id: string, data: Record<string, unknown>, options: { returnDocument: 'after' }) => Promise<unknown> }" : "{ update: (id: string, data: Record<string, unknown>) => Promise<unknown> }"},
|
|
632
654
|
id: string,
|
|
633
655
|
): Promise<string> {
|
|
634
656
|
const refreshToken = crypto.randomBytes(40).toString('hex');
|
|
@@ -652,7 +674,19 @@ export const POST = async (req: Request, res: Response) => {
|
|
|
652
674
|
${loginBody}
|
|
653
675
|
};
|
|
654
676
|
` : `import { issueToken, verifyPassword } from 'express-file-cluster/auth';
|
|
655
|
-
|
|
677
|
+
import { rateLimit } from 'express-file-cluster';
|
|
678
|
+
${loginDbImports}${loginMeta}// 10 attempts per 15 minutes per address and account. Keyed on the email too, so behind a
|
|
679
|
+
// proxy without TRUST_PROXY (every client then has the proxy's IP) it still limits one
|
|
680
|
+
// account at a time, never everyone.
|
|
681
|
+
export const middlewares = [
|
|
682
|
+
rateLimit({
|
|
683
|
+
window: '15m',
|
|
684
|
+
max: 10,
|
|
685
|
+
key: (req) => \`\${req.ip}:\${String(req.body?.email ?? '').toLowerCase()}\`,
|
|
686
|
+
}),
|
|
687
|
+
];
|
|
688
|
+
|
|
689
|
+
const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
|
|
656
690
|
|
|
657
691
|
async function issueRefreshToken(res, model, id) {
|
|
658
692
|
const refreshToken = crypto.randomBytes(40).toString('hex');
|
|
@@ -1612,7 +1646,13 @@ ${hasAdmin ? `import { Admin } from '../../model/Admin.js';
|
|
|
1612
1646
|
else if (admin) await ${db.update("Admin", `admin.id`, `{ resetToken, resetTokenExpiry }`)};` : ` await ${db.update("User", `user.id`, `{ resetToken, resetTokenExpiry }`)};`;
|
|
1613
1647
|
const fpContent = ts ? `import type { Request, Response } from 'express';
|
|
1614
1648
|
import crypto from 'node:crypto';
|
|
1615
|
-
|
|
1649
|
+
import { rateLimit } from 'express-file-cluster';
|
|
1650
|
+
${mailerTaskImport}${fpModelImports}${fpMeta}// 5 reset emails per hour per address \u2014 keeps the endpoint from mail-bombing someone.
|
|
1651
|
+
export const middlewares = [
|
|
1652
|
+
rateLimit({ window: '1h', max: 5, key: (req) => String(req.body?.email ?? '').toLowerCase() }),
|
|
1653
|
+
];
|
|
1654
|
+
|
|
1655
|
+
const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
1616
1656
|
|
|
1617
1657
|
export const POST = async (req: Request, res: Response) => {
|
|
1618
1658
|
const { email } = req.body;
|
|
@@ -1632,7 +1672,13 @@ ${sendResetEmail} }
|
|
|
1632
1672
|
res.json({ message: 'Reset email sent' });
|
|
1633
1673
|
};
|
|
1634
1674
|
` : `import crypto from 'node:crypto';
|
|
1635
|
-
|
|
1675
|
+
import { rateLimit } from 'express-file-cluster';
|
|
1676
|
+
${mailerTaskImport}${fpModelImports}${fpMeta}// 5 reset emails per hour per address \u2014 keeps the endpoint from mail-bombing someone.
|
|
1677
|
+
export const middlewares = [
|
|
1678
|
+
rateLimit({ window: '1h', max: 5, key: (req) => String(req.body?.email ?? '').toLowerCase() }),
|
|
1679
|
+
];
|
|
1680
|
+
|
|
1681
|
+
const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
|
|
1636
1682
|
|
|
1637
1683
|
export const POST = async (req, res) => {
|
|
1638
1684
|
const { email } = req.body;
|
|
@@ -4097,6 +4143,12 @@ var VARIANTS = ["typescript", "javascript"].flatMap((language) => {
|
|
|
4097
4143
|
database: "mongodb",
|
|
4098
4144
|
modelApi: "defineModel"
|
|
4099
4145
|
},
|
|
4146
|
+
{
|
|
4147
|
+
key: `${lang}-mongo-collection`,
|
|
4148
|
+
language,
|
|
4149
|
+
database: "mongodb",
|
|
4150
|
+
modelApi: "defineCollection"
|
|
4151
|
+
},
|
|
4100
4152
|
{
|
|
4101
4153
|
key: `${lang}-pg-model`,
|
|
4102
4154
|
language,
|
|
@@ -4512,7 +4564,7 @@ function registryOptions(v, items = [], extra = {}) {
|
|
|
4512
4564
|
projectName: "efc-app",
|
|
4513
4565
|
language: v.language,
|
|
4514
4566
|
database: v.database,
|
|
4515
|
-
|
|
4567
|
+
modelApi: v.modelApi,
|
|
4516
4568
|
authStrategy: "http-only",
|
|
4517
4569
|
cluster: true,
|
|
4518
4570
|
tasks: true,
|
|
@@ -4555,7 +4607,7 @@ async function writeManifest(dest, opts, items) {
|
|
|
4555
4607
|
registry: (pkg.dependencies["express-file-cluster"] ?? "").replace(/^[^\d]*/, ""),
|
|
4556
4608
|
language: opts.language,
|
|
4557
4609
|
database: opts.database,
|
|
4558
|
-
modelApi: opts.database === "postgresql" ? opts.modelApi ?? "defineTable" : "defineModel",
|
|
4610
|
+
modelApi: opts.database === "postgresql" ? opts.modelApi ?? "defineTable" : opts.modelApi === "defineModel" ? "defineModel" : "defineCollection",
|
|
4559
4611
|
tasks: opts.tasks ? opts.taskBackend ?? "bullmq" : null,
|
|
4560
4612
|
items: [...withDependencies(items)].sort(),
|
|
4561
4613
|
files: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)))
|
|
@@ -4651,22 +4703,30 @@ async function main() {
|
|
|
4651
4703
|
]
|
|
4652
4704
|
});
|
|
4653
4705
|
if (p.isCancel(database)) cancel2();
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4706
|
+
const api = await p.select({
|
|
4707
|
+
message: "Models:",
|
|
4708
|
+
options: database === "postgresql" ? [
|
|
4709
|
+
{
|
|
4710
|
+
value: "defineTable",
|
|
4711
|
+
label: "defineTable",
|
|
4712
|
+
hint: "PostgreSQL-native \u2014 foreign keys, relations, include, Prisma-style queries"
|
|
4713
|
+
},
|
|
4714
|
+
{ value: "defineModel", label: "defineModel", hint: "portable \u2014 the same API on MongoDB" }
|
|
4715
|
+
] : [
|
|
4716
|
+
{
|
|
4717
|
+
value: "defineCollection",
|
|
4718
|
+
label: "defineCollection",
|
|
4719
|
+
hint: "MongoDB-native \u2014 mongoose-style queries, typed pipelines, BSON types"
|
|
4720
|
+
},
|
|
4721
|
+
{
|
|
4722
|
+
value: "defineModel",
|
|
4723
|
+
label: "defineModel",
|
|
4724
|
+
hint: "portable \u2014 the same API on PostgreSQL"
|
|
4725
|
+
}
|
|
4726
|
+
]
|
|
4727
|
+
});
|
|
4728
|
+
if (p.isCancel(api)) cancel2();
|
|
4729
|
+
const modelApi2 = api;
|
|
4670
4730
|
const taskBackend = await p.select({
|
|
4671
4731
|
message: "Task queue backend:",
|
|
4672
4732
|
options: database === "postgresql" ? [
|