From 437cf9a45a4181eaffe7ae999a1fc6b41af3237b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Alberto=20G=C3=B3mez=20Garc=C3=ADa?= <90482689+modejota@users.noreply.github.com> Date: Fri, 2 May 2025 15:23:04 +0200 Subject: [PATCH] add mongoose orm to the stack builder (#191) --- .changeset/floppy-fans-cross.md | 5 + apps/cli/src/constants.ts | 2 + apps/cli/src/helpers/db-setup.ts | 6 + apps/cli/src/helpers/template-manager.ts | 5 + apps/cli/src/index.ts | 148 +++---- apps/cli/src/prompts/orm.ts | 42 +- apps/cli/src/types.ts | 2 +- .../auth/server/base/src/lib/auth.ts.hbs | 25 ++ .../mongodb/src/db/models/auth.model.ts | 68 +++ .../db/mongoose/mongodb/src/db/index.ts | 6 + .../mongoose/base/src/routers/todo.ts.hbs | 66 +++ .../mongodb/src/db/models/todo.model.ts | 24 ++ apps/web/public/icon/mongoose.svg | 4 + .../app/(home)/_components/StackArchitech.tsx | 387 +++++++++--------- apps/web/src/lib/constant.ts | 7 + 15 files changed, 524 insertions(+), 273 deletions(-) create mode 100644 .changeset/floppy-fans-cross.md create mode 100644 apps/cli/templates/auth/server/db/mongoose/mongodb/src/db/models/auth.model.ts create mode 100644 apps/cli/templates/db/mongoose/mongodb/src/db/index.ts create mode 100644 apps/cli/templates/examples/todo/server/mongoose/base/src/routers/todo.ts.hbs create mode 100644 apps/cli/templates/examples/todo/server/mongoose/mongodb/src/db/models/todo.model.ts create mode 100644 apps/web/public/icon/mongoose.svg diff --git a/.changeset/floppy-fans-cross.md b/.changeset/floppy-fans-cross.md new file mode 100644 index 0000000..190124d --- /dev/null +++ b/.changeset/floppy-fans-cross.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add mongoose orm support to the stack builder diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index db92c2b..64e0a67 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -40,6 +40,8 @@ export const dependencyVersionMap = { "@prisma/client": "^6.7.0", prisma: "^6.7.0", + mongoose: "^8.14.0", + "vite-plugin-pwa": "^0.21.2", "@vite-pwa/assets-generator": "^0.2.6", diff --git a/apps/cli/src/helpers/db-setup.ts b/apps/cli/src/helpers/db-setup.ts index f6dc93d..e4d6288 100644 --- a/apps/cli/src/helpers/db-setup.ts +++ b/apps/cli/src/helpers/db-setup.ts @@ -62,6 +62,12 @@ export async function setupDatabase(config: ProjectConfig): Promise { projectDir: serverDir, }); } + } else if (orm === "mongoose") { + await addPackageDependency({ + dependencies: ["mongoose"], + devDependencies: [], + projectDir: serverDir, + }); } if (database === "sqlite" && dbSetup === "turso") { diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index e1767ab..79c42ad 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -367,6 +367,11 @@ export async function setupAuthTemplate( PKG_ROOT, `templates/auth/server/db/prisma/${db}`, ); + } else if (orm === "mongoose") { + authDbSrc = path.join( + PKG_ROOT, + `templates/auth/server/db/mongoose/${db}`, + ) } if (authDbSrc && (await fs.pathExists(authDbSrc))) { await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 3a23e9a..b32503a 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -60,7 +60,7 @@ async function main() { .option("orm", { type: "string", describe: "ORM type", - choices: ["drizzle", "prisma", "none"], + choices: ["drizzle", "prisma", "mongoose", "none"], }) .option("auth", { type: "boolean", @@ -446,78 +446,82 @@ function processAndValidateFlags( config.dbSetup = "none"; } - if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") { + if (effectiveDatabase === "mongodb" && effectiveOrm === "drizzle") { + consola.fatal( + "Drizzle ORM is not compatible with MongoDB. Please use --orm prisma or --orm mongoose.", + ); + process.exit(1); + } + + if ( + effectiveOrm === "mongoose" && + effectiveDatabase && + effectiveDatabase !== "mongodb" + ) { + consola.fatal( + `Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`, + ); + process.exit(1); + } + + if (config.dbSetup && config.dbSetup !== "none") { + const dbSetup = config.dbSetup; + + if (!effectiveDatabase || effectiveDatabase === "none") { consola.fatal( - "MongoDB is only available with Prisma. Cannot use --database mongodb with --orm drizzle", + `Database setup '--db-setup ${dbSetup}' requires a database. Cannot use when database is 'none'.`, ); process.exit(1); } - if (config.dbSetup && config.dbSetup !== "none") { - const dbSetup = config.dbSetup; - if (dbSetup === "turso") { - if (effectiveDatabase && effectiveDatabase !== "sqlite") { - consola.fatal( - `Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`, - ); - process.exit(1); - } - if (effectiveOrm === "prisma") { - consola.fatal( - "Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma", - ); - process.exit(1); - } - config.database = "sqlite"; - config.orm = "drizzle"; - } else if (dbSetup === "prisma-postgres") { - if (effectiveDatabase && effectiveDatabase !== "postgres") { - consola.fatal( - `Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`, - ); - process.exit(1); - } - if ( - effectiveOrm && - effectiveOrm !== "prisma" && - effectiveOrm !== "none" - ) { - consola.fatal( - `Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`, - ); - process.exit(1); - } - config.database = "postgres"; - config.orm = "prisma"; - } else if (dbSetup === "mongodb-atlas") { - if (effectiveDatabase && effectiveDatabase !== "mongodb") { - consola.fatal( - `MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`, - ); - process.exit(1); - } - if ( - effectiveOrm && - effectiveOrm !== "prisma" && - effectiveOrm !== "none" - ) { - consola.fatal( - `MongoDB Atlas setup requires Prisma ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`, - ); - process.exit(1); - } - config.database = "mongodb"; - config.orm = "prisma"; - } else if (dbSetup === "neon") { - if (effectiveDatabase && effectiveDatabase !== "postgres") { - consola.fatal( - `Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`, - ); - process.exit(1); - } - config.database = "postgres"; + if (dbSetup === "turso") { + if (effectiveDatabase && effectiveDatabase !== "sqlite") { + consola.fatal( + `Turso setup requires SQLite. Cannot use --db-setup turso with --database ${effectiveDatabase}`, + ); + process.exit(1); + } + if (effectiveOrm !== "drizzle") { + consola.fatal( + `Turso setup requires Drizzle ORM. Cannot use --db-setup turso with --orm ${effectiveOrm ?? "none"}.`, + ); + process.exit(1); + } + } else if (dbSetup === "prisma-postgres") { + if (effectiveDatabase !== "postgres") { + consola.fatal( + `Prisma PostgreSQL setup requires PostgreSQL. Cannot use --db-setup prisma-postgres with --database ${effectiveDatabase}.`, + ); + process.exit(1); + } + if (effectiveOrm !== "prisma") { + consola.fatal( + `Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with --orm ${effectiveOrm}.`, + ); + process.exit(1); + } + } else if (dbSetup === "mongodb-atlas") { + if (effectiveDatabase !== "mongodb") { + consola.fatal( + `MongoDB Atlas setup requires MongoDB. Cannot use --db-setup mongodb-atlas with --database ${effectiveDatabase}.`, + ); + process.exit(1); + } + if (effectiveOrm !== "prisma" && effectiveOrm !== "mongoose") { + consola.fatal( + `MongoDB Atlas setup requires Prisma or Mongoose ORM. Cannot use --db-setup mongodb-atlas with --orm ${effectiveOrm}.`, + ); + process.exit(1); + } + } else if (dbSetup === "neon") { + if (effectiveDatabase !== "postgres") { + consola.fatal( + `Neon PostgreSQL setup requires PostgreSQL. Cannot use --db-setup neon with --database ${effectiveDatabase}.`, + ); + process.exit(1); } } + } const includesNuxt = effectiveFrontend?.includes("nuxt"); const includesSvelte = effectiveFrontend?.includes("svelte"); @@ -576,13 +580,13 @@ function processAndValidateFlags( process.exit(1); } - if (config.addons.includes("husky") && !config.addons.includes("biome")) { - consola.warn( - "Husky addon is recommended to be used with Biome for lint-staged configuration.", - ); - } - config.addons = [...new Set(config.addons)]; + if (config.addons.includes("husky") && !config.addons.includes("biome")) { + consola.warn( + "Husky addon is recommended to be used with Biome for lint-staged configuration.", + ); } + config.addons = [...new Set(config.addons)]; + } const onlyNativeFrontend = effectiveFrontend && diff --git a/apps/cli/src/prompts/orm.ts b/apps/cli/src/prompts/orm.ts index 8dc0628..2d58d8c 100644 --- a/apps/cli/src/prompts/orm.ts +++ b/apps/cli/src/prompts/orm.ts @@ -3,6 +3,24 @@ import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; import type { ProjectBackend, ProjectDatabase, ProjectOrm } from "../types"; +const ormOptions = { + prisma: { + value: "prisma" as const, + label: "Prisma", + hint: "Powerful, feature-rich ORM", + }, + mongoose: { + value: "mongoose" as const, + label: "Mongoose", + hint: "Elegant object modeling tool", + }, + drizzle: { + value: "drizzle" as const, + label: "Drizzle", + hint: "Lightweight and performant TypeScript ORM", + }, +}; + export async function getORMChoice( orm: ProjectOrm | undefined, hasDatabase: boolean, @@ -16,26 +34,16 @@ export async function getORMChoice( if (!hasDatabase) return "none"; if (orm !== undefined) return orm; - if (database === "mongodb") { - log.info("Only Prisma is supported with MongoDB."); - return "prisma"; - } + const options = [ + ...(database === "mongodb" + ? [ormOptions.prisma, ormOptions.mongoose] + : [ormOptions.drizzle, ormOptions.prisma]), + ]; const response = await select({ message: "Select ORM", - options: [ - { - value: "drizzle", - label: "Drizzle", - hint: "lightweight and performant TypeScript ORM", - }, - { - value: "prisma", - label: "Prisma", - hint: "Powerful, feature-rich ORM", - }, - ], - initialValue: DEFAULT_CONFIG.orm, + options, + initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm, }); if (isCancel(response)) { diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index e3f679f..1ce2547 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -4,7 +4,7 @@ export type ProjectDatabase = | "mongodb" | "mysql" | "none"; -export type ProjectOrm = "drizzle" | "prisma" | "none"; +export type ProjectOrm = "drizzle" | "prisma" | "mongoose" | "none"; export type ProjectPackageManager = "npm" | "pnpm" | "bun"; export type ProjectAddons = | "pwa" diff --git a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs index 61578c6..6ea57c3 100644 --- a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs +++ b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs @@ -58,6 +58,31 @@ export const auth = betterAuth({ }); {{/if}} +{{#if (eq orm "mongoose")}} +import { betterAuth } from "better-auth"; +import { mongodbAdapter } from "better-auth/adapters/mongodb"; +{{#if (includes frontend "native")}} +import { expo } from "@better-auth/expo"; +{{/if}} +import { client } from "../db"; + +export const auth = betterAuth({ + database: mongodbAdapter(client.db()), + trustedOrigins: [ + process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} + "my-better-t-app://",{{/if}} + ], + emailAndPassword: { + enabled: true, + } + + {{~#if (includes frontend "native")}} + , + plugins: [expo()] + {{/if~}} +}); +{{/if}} + {{#if (eq orm "none")}} import { betterAuth } from "better-auth"; {{#if (includes frontend "native")}} diff --git a/apps/cli/templates/auth/server/db/mongoose/mongodb/src/db/models/auth.model.ts b/apps/cli/templates/auth/server/db/mongoose/mongodb/src/db/models/auth.model.ts new file mode 100644 index 0000000..bb04aed --- /dev/null +++ b/apps/cli/templates/auth/server/db/mongoose/mongodb/src/db/models/auth.model.ts @@ -0,0 +1,68 @@ +import mongoose from 'mongoose'; + +const { Schema, model } = mongoose; + +const userSchema = new Schema( + { + _id: { type: String }, + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + emailVerified: { type: Boolean, required: true }, + image: { type: String }, + createdAt: { type: Date, required: true }, + updatedAt: { type: Date, required: true }, + }, + { collection: 'user' } +); + +const sessionSchema = new Schema( + { + _id: { type: String }, + expiresAt: { type: Date, required: true }, + token: { type: String, required: true, unique: true }, + createdAt: { type: Date, required: true }, + updatedAt: { type: Date, required: true }, + ipAddress: { type: String }, + userAgent: { type: String }, + userId: { type: String, ref: 'User', required: true }, + }, + { collection: 'session' } +); + +const accountSchema = new Schema( + { + _id: { type: String }, + accountId: { type: String, required: true }, + providerId: { type: String, required: true }, + userId: { type: String, ref: 'User', required: true }, + accessToken: { type: String }, + refreshToken: { type: String }, + idToken: { type: String }, + accessTokenExpiresAt: { type: Date }, + refreshTokenExpiresAt: { type: Date }, + scope: { type: String }, + password: { type: String }, + createdAt: { type: Date, required: true }, + updatedAt: { type: Date, required: true }, + }, + { collection: 'account' } +); + +const verificationSchema = new Schema( + { + _id: { type: String }, + identifier: { type: String, required: true }, + value: { type: String, required: true }, + expiresAt: { type: Date, required: true }, + createdAt: { type: Date }, + updatedAt: { type: Date }, + }, + { collection: 'verification' } +); + +const User = model('User', userSchema); +const Session = model('Session', sessionSchema); +const Account = model('Account', accountSchema); +const Verification = model('Verification', verificationSchema); + +export { User, Session, Account, Verification }; diff --git a/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts b/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts new file mode 100644 index 0000000..82b3b6b --- /dev/null +++ b/apps/cli/templates/db/mongoose/mongodb/src/db/index.ts @@ -0,0 +1,6 @@ +import mongoose from 'mongoose'; + +await mongoose.connect(process.env.DATABASE_URL || ""); +const client = mongoose.connection.getClient(); + +export { client }; \ No newline at end of file diff --git a/apps/cli/templates/examples/todo/server/mongoose/base/src/routers/todo.ts.hbs b/apps/cli/templates/examples/todo/server/mongoose/base/src/routers/todo.ts.hbs new file mode 100644 index 0000000..0f79624 --- /dev/null +++ b/apps/cli/templates/examples/todo/server/mongoose/base/src/routers/todo.ts.hbs @@ -0,0 +1,66 @@ +{{#if (eq api "orpc")}} +import { z } from "zod"; +import { publicProcedure } from "../lib/orpc"; +import { Todo } from "../db/models/todo.model"; + +export const todoRouter = { + getAll: publicProcedure.handler(async () => { + return await Todo.find().lean(); + }), + + create: publicProcedure + .input(z.object({ text: z.string().min(1) })) + .handler(async ({ input }) => { + const newTodo = await Todo.create({ text: input.text }); + return newTodo.toObject(); + }), + + toggle: publicProcedure + .input(z.object({ id: z.string(), completed: z.boolean() })) + .handler(async ({ input }) => { + await Todo.updateOne({ id: input.id }, { completed: input.completed }); + return { success: true }; + }), + + delete: publicProcedure + .input(z.object({ id: z.string() })) + .handler(async ({ input }) => { + await Todo.deleteOne({ id: input.id }); + return { success: true }; + }), +}; + +{{/if}} + +{{#if (eq api "trpc")}} +import { z } from "zod"; +import { router, publicProcedure } from "../lib/trpc"; +import { Todo } from "../db/models/todo.model"; + +export const todoRouter = router({ + getAll: publicProcedure.query(async () => { + return await Todo.find().lean(); + }), + + create: publicProcedure + .input(z.object({ text: z.string().min(1) })) + .mutation(async ({ input }) => { + const newTodo = await Todo.create({ text: input.text }); + return newTodo.toObject(); + }), + + toggle: publicProcedure + .input(z.object({ id: z.string(), completed: z.boolean() })) + .mutation(async ({ input }) => { + await Todo.updateOne({ id: input.id }, { completed: input.completed }); + return { success: true }; + }), + + delete: publicProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input }) => { + await Todo.deleteOne({ id: input.id }); + return { success: true }; + }), +}); +{{/if}} diff --git a/apps/cli/templates/examples/todo/server/mongoose/mongodb/src/db/models/todo.model.ts b/apps/cli/templates/examples/todo/server/mongoose/mongodb/src/db/models/todo.model.ts new file mode 100644 index 0000000..2e96563 --- /dev/null +++ b/apps/cli/templates/examples/todo/server/mongoose/mongodb/src/db/models/todo.model.ts @@ -0,0 +1,24 @@ +import mongoose from 'mongoose'; + +const { Schema, model } = mongoose; + +const todoSchema = new Schema({ + id: { + type: mongoose.Schema.Types.ObjectId, + auto: true, + }, + text: { + type: String, + required: true, + }, + completed: { + type: Boolean, + default: false, + }, +}, { + collection: 'todo' +}); + +const Todo = model('Todo', todoSchema); + +export { Todo }; diff --git a/apps/web/public/icon/mongoose.svg b/apps/web/public/icon/mongoose.svg new file mode 100644 index 0000000..f84bcc2 --- /dev/null +++ b/apps/web/public/icon/mongoose.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index e4a82b4..b0445e0 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -224,137 +224,158 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { changed = true; } - if (nextStack.database === "none") { - if (nextStack.orm !== "none") { - notes.database.notes.push( - "Database 'None' selected: ORM will be set to 'None'.", - ); - notes.orm.notes.push( - "ORM requires a database. It will be set to 'None'.", - ); - notes.database.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "none"; - changed = true; - } - if (nextStack.auth === "true") { - notes.database.notes.push( - "Database 'None' selected: Auth will be disabled.", - ); - notes.auth.notes.push( - "Authentication requires a database. It will be disabled.", - ); - notes.database.hasIssue = true; - notes.auth.hasIssue = true; - nextStack.auth = "false"; - changed = true; - } - if (nextStack.dbSetup !== "none") { - notes.database.notes.push( - "Database 'None' selected: DB Setup will be set to 'Basic'.", - ); - notes.dbSetup.notes.push( - "DB Setup requires a database. It will be set to 'Basic Setup'.", - ); - notes.database.hasIssue = true; - notes.dbSetup.hasIssue = true; - nextStack.dbSetup = "none"; - changed = true; - } - } else if (nextStack.database === "mongodb") { - if (nextStack.orm !== "prisma" && nextStack.orm !== "none") { - notes.database.notes.push( - "MongoDB requires Prisma ORM. It will be selected.", - ); - notes.orm.notes.push( - "MongoDB requires Prisma ORM. It will be selected.", - ); - notes.database.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - } + if (nextStack.database === "none") { + if (nextStack.orm !== "none") { + notes.database.notes.push( + "Database 'None' selected: ORM will be set to 'None'.", + ); + notes.orm.notes.push( + "ORM requires a database. It will be set to 'None'.", + ); + notes.database.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "none"; + changed = true; } + if (nextStack.auth === "true") { + notes.database.notes.push( + "Database 'None' selected: Auth will be disabled.", + ); + notes.auth.notes.push( + "Authentication requires a database. It will be disabled.", + ); + notes.database.hasIssue = true; + notes.auth.hasIssue = true; + nextStack.auth = "false"; + changed = true; + } + if (nextStack.dbSetup !== "none") { + notes.database.notes.push( + "Database 'None' selected: DB Setup will be set to 'Basic'.", + ); + notes.dbSetup.notes.push( + "DB Setup requires a database. It will be set to 'Basic Setup'.", + ); + notes.database.hasIssue = true; + notes.dbSetup.hasIssue = true; + nextStack.dbSetup = "none"; + changed = true; + } + } else if (nextStack.database === "mongodb") { + if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { + notes.database.notes.push( + "MongoDB requires Prisma or Mongoose ORM. Prisma will be selected.", + ); + notes.orm.notes.push("MongoDB requires Prisma or Mongoose ORM. Prisma will be selected."); + notes.database.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + } + } else { + if (nextStack.orm === "mongoose") { + notes.database.notes.push( + "Relational databases are not compatible with Mongoose ORM", + ); + notes.orm.notes.push("Relational databases are not compatible with Mongoose ORM"); + notes.database.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + } + if (nextStack.dbSetup === "mongodb-atlas") { + notes.database.notes.push( + "Relational databases are not compatible with MongoDB Atlas setup. DB Setup will be reset.", + ); + notes.dbSetup.notes.push( + "MongoDB Atlas setup requires MongoDB. It will be reset to 'Basic Setup'.", + ); + notes.database.hasIssue = true; + notes.dbSetup.hasIssue = true; + nextStack.dbSetup = "none"; + changed = true; + } + } - if (nextStack.dbSetup === "turso") { - if (nextStack.database !== "sqlite") { - notes.dbSetup.notes.push("Turso requires SQLite. It will be selected."); - notes.database.notes.push( - "Turso DB setup requires SQLite. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "sqlite"; - changed = true; - } - if (nextStack.orm !== "drizzle") { - notes.dbSetup.notes.push( - "Turso requires Drizzle ORM. It will be selected.", - ); - notes.orm.notes.push( - "Turso DB setup requires Drizzle ORM. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "drizzle"; - changed = true; - } - } else if (nextStack.dbSetup === "prisma-postgres") { - if (nextStack.database !== "postgres") { - notes.dbSetup.notes.push("Requires PostgreSQL. It will be selected."); - notes.database.notes.push( - "Prisma PostgreSQL setup requires PostgreSQL. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "postgres"; - changed = true; - } - if (nextStack.orm !== "prisma") { - notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected."); - notes.orm.notes.push( - "Prisma PostgreSQL setup requires Prisma ORM. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - } - } else if (nextStack.dbSetup === "mongodb-atlas") { - if (nextStack.database !== "mongodb") { - notes.dbSetup.notes.push("Requires MongoDB. It will be selected."); - notes.database.notes.push( - "MongoDB Atlas setup requires MongoDB. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "mongodb"; - changed = true; - } - if (nextStack.orm !== "prisma") { - notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected."); - notes.orm.notes.push( - "MongoDB Atlas setup requires Prisma ORM. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.orm.hasIssue = true; - nextStack.orm = "prisma"; - changed = true; - } - } else if (nextStack.dbSetup === "neon") { - if (nextStack.database !== "postgres") { - notes.dbSetup.notes.push( - "Neon requires PostgreSQL. It will be selected.", - ); - notes.database.notes.push( - "Neon DB setup requires PostgreSQL. It will be selected.", - ); - notes.dbSetup.hasIssue = true; - notes.database.hasIssue = true; - nextStack.database = "postgres"; - changed = true; - } + if (nextStack.dbSetup === "turso") { + if (nextStack.database !== "sqlite") { + notes.dbSetup.notes.push("Turso requires SQLite. It will be selected."); + notes.database.notes.push( + "Turso DB setup requires SQLite. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "sqlite"; + changed = true; } + if (nextStack.orm !== "drizzle") { + notes.dbSetup.notes.push( + "Turso requires Drizzle ORM. It will be selected.", + ); + notes.orm.notes.push( + "Turso DB setup requires Drizzle ORM. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "drizzle"; + changed = true; + } + } else if (nextStack.dbSetup === "prisma-postgres") { + if (nextStack.database !== "postgres") { + notes.dbSetup.notes.push("Requires PostgreSQL. It will be selected."); + notes.database.notes.push( + "Prisma PostgreSQL setup requires PostgreSQL. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "postgres"; + changed = true; + } + if (nextStack.orm !== "prisma") { + notes.dbSetup.notes.push("Requires Prisma ORM. It will be selected."); + notes.orm.notes.push( + "Prisma PostgreSQL setup requires Prisma ORM. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + } + } else if (nextStack.dbSetup === "mongodb-atlas") { + if (nextStack.database !== "mongodb") { + notes.dbSetup.notes.push("Requires MongoDB. It will be selected."); + notes.database.notes.push( + "MongoDB Atlas setup requires MongoDB. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "mongodb"; + changed = true; + } + if (nextStack.orm !== "prisma" && nextStack.orm !== "mongoose") { + notes.dbSetup.notes.push("Requires Prisma or Mongoose ORM. Prisma will be selected."); + notes.orm.notes.push( + "MongoDB Atlas setup requires Prisma or Mongoose ORM. Prisma will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "prisma"; + changed = true; + } + } else if (nextStack.dbSetup === "neon") { + if (nextStack.database !== "postgres") { + notes.dbSetup.notes.push( + "Neon requires PostgreSQL. It will be selected.", + ); + notes.database.notes.push( + "Neon DB setup requires PostgreSQL. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "postgres"; + changed = true; + } + } const isNuxt = nextStack.frontend.includes("nuxt"); const isSvelte = nextStack.frontend.includes("svelte"); @@ -450,51 +471,48 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } } - const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)]; - if (uniqueIncompatibleExamples.length > 0) { - if (isNativeOnly) { - } else { - if ( - !isWeb && - (uniqueIncompatibleExamples.includes("todo") || - uniqueIncompatibleExamples.includes("ai")) - ) { - notes.frontend.notes.push( - "Examples require a web frontend. Incompatible examples will be removed.", - ); - notes.examples.notes.push( - "Requires a web frontend. Incompatible examples will be removed.", - ); - notes.frontend.hasIssue = true; - notes.examples.hasIssue = true; - } - if ( - nextStack.database === "none" && - uniqueIncompatibleExamples.includes("todo") - ) { - notes.database.notes.push( - "Todo example requires a database. It will be removed.", - ); - notes.examples.notes.push( - "Todo example requires a database. It will be removed.", - ); - notes.database.hasIssue = true; - notes.examples.hasIssue = true; - } - if ( - nextStack.backend === "elysia" && - uniqueIncompatibleExamples.includes("ai") - ) { - notes.backend.notes.push( - "AI example is not compatible with Elysia. It will be removed.", - ); - notes.examples.notes.push( - "AI example is not compatible with Elysia. It will be removed.", - ); - notes.backend.hasIssue = true; - notes.examples.hasIssue = true; - } - } + const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)]; + if (uniqueIncompatibleExamples.length > 0) { + if ( + !isWeb && + (uniqueIncompatibleExamples.includes("todo") || + uniqueIncompatibleExamples.includes("ai")) + ) { + notes.frontend.notes.push( + "Examples require a web frontend. Incompatible examples will be removed.", + ); + notes.examples.notes.push( + "Requires a web frontend. Incompatible examples will be removed.", + ); + notes.frontend.hasIssue = true; + notes.examples.hasIssue = true; + } + if ( + nextStack.database === "none" && + uniqueIncompatibleExamples.includes("todo") + ) { + notes.database.notes.push( + "Todo example requires a database. It will be removed.", + ); + notes.examples.notes.push( + "Todo example requires a database. It will be removed.", + ); + notes.database.hasIssue = true; + notes.examples.hasIssue = true; + } + if ( + nextStack.backend === "elysia" && + uniqueIncompatibleExamples.includes("ai") + ) { + notes.backendFramework.notes.push( + "AI example is not compatible with Elysia. It will be removed.", + ); + notes.examples.notes.push( + "AI example is not compatible with Elysia. It will be removed.", + ); + notes.backendFramework.hasIssue = true; + notes.examples.hasIssue = true; + } const originalExamplesLength = nextStack.examples.length; nextStack.examples = nextStack.examples.filter( @@ -748,9 +766,10 @@ const StackArchitect = () => { if ( currentStack.database === "mongodb" && techId !== "prisma" && + techId !== "mongoose" && techId !== "none" ) - reason = "MongoDB requires the Prisma ORM."; + reason = "MongoDB requires the Prisma or Mongoose ORM."; if ( currentStack.dbSetup === "turso" && techId !== "drizzle" && @@ -766,21 +785,23 @@ const StackArchitect = () => { if ( currentStack.dbSetup === "mongodb-atlas" && techId !== "prisma" && + techId !== "mongoose" && techId !== "none" ) - reason = "MongoDB Atlas setup requires Prisma ORM."; + reason = "MongoDB Atlas setup requires Prisma or Mongoose ORM."; if (techId === "none") { if (currentStack.database === "mongodb") - reason = "MongoDB requires Prisma ORM."; + reason = "MongoDB requires Prisma or Mongoose ORM."; if (currentStack.dbSetup === "turso") reason = "Turso DB setup requires Drizzle ORM."; - if ( - currentStack.dbSetup === "prisma-postgres" || - currentStack.dbSetup === "mongodb-atlas" - ) + if (currentStack.dbSetup === "prisma-postgres") reason = "This DB setup requires Prisma ORM."; } + + if (techId === "mongoose" && (currentStack.database !== "mongodb")) { + reason = "Mongoose ORM is not compatible with relational databases."; + } } if (catKey === "dbSetup" && techId !== "none") { @@ -809,8 +830,8 @@ const StackArchitect = () => { currentStack.database !== "none" ) reason = "Requires MongoDB database."; - if (currentStack.orm !== "prisma" && currentStack.orm !== "none") - reason = "Requires Prisma ORM."; + if (currentStack.orm !== "prisma" && currentStack.orm !== "mongoose" && currentStack.orm !== "none") + reason = "Requires Prisma or Mongoose ORM."; } else if (techId === "neon") { if ( currentStack.database !== "postgres" && @@ -1051,7 +1072,7 @@ const StackArchitect = () => { }); }; - const copyToClipboard = () => { + const copyToClipboard = () => { navigator.clipboard.writeText(command); setCopied(true); setTimeout(() => setCopied(false), 2000); diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 09133c8..df8f970 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -192,6 +192,13 @@ export const TECH_OPTIONS = { icon: "/icon/prisma.svg", color: "from-purple-400 to-purple-600", }, + { + id: "mongoose", + name: "Mongoose", + description: "Elegant object modeling tool", + icon: "/icon/mongoose.svg", + color: "from-blue-400 to-blue-600", + }, { id: "none", name: "No ORM",