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 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.9.6"
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 \`defineModel\` models.`}
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${smtpExample}${oauthExample}`;
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
- ${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
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
- ${loginDbImports}${loginMeta}const REFRESH_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30; // 30 days
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
- ${mailerTaskImport}${fpModelImports}${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
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
- ${mailerTaskImport}${fpModelImports}${fpMeta}const RESET_TOKEN_TTL_MS = 1000 * 60 * 60; // 1 hour
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
- ...v.database === "postgresql" && { modelApi: v.modelApi },
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
- let modelApi2;
4655
- if (database === "postgresql") {
4656
- const api = await p.select({
4657
- message: "Models:",
4658
- options: [
4659
- {
4660
- value: "defineTable",
4661
- label: "defineTable",
4662
- hint: "PostgreSQL-native \u2014 foreign keys, relations, include, Prisma-style queries"
4663
- },
4664
- { value: "defineModel", label: "defineModel", hint: "portable \u2014 the same API on MongoDB" }
4665
- ]
4666
- });
4667
- if (p.isCancel(api)) cancel2();
4668
- modelApi2 = api;
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" ? [