UNPKG

nuxt-mongoose

Version:

Nuxt 3 module for MongoDB with Mongoose

336 lines (325 loc) 11.2 kB
import { defineNuxtModule, logger, createResolver, addTemplate, addServerPlugin } from '@nuxt/kit'; import defu from 'defu'; import { join } from 'pathe'; import mongoose from 'mongoose'; import { $fetch } from 'ofetch'; import { existsSync } from 'node:fs'; import { onDevToolsInitialized, extendServerRpc } from '@nuxt/devtools-kit'; import plrz from 'pluralize'; import fs from 'fs-extra'; const version = "1.0.6"; function useViteWebSocket(nuxt) { return new Promise((_resolve) => { nuxt.hooks.hook("vite:serverCreated", (viteServer) => { _resolve(viteServer.ws); }); }); } function pluralize(str) { return plrz.plural(str); } function singularize(str) { return plrz.singular(str); } function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function generateSchemaFile(name, fields) { name = capitalize(name); const outputObject = JSON.stringify( fields.reduce((acc, curr) => { const { name: name2, ...rest } = curr; acc[name2] = rest; return acc; }, {}), null, 2 ).replace(/"([^"]+)":/g, "$1:").replace(/"(\w+)":/g, "$1:").replace(/\s*"\w+":/g, (match) => match.trim()).replace(/"string"/g, "'string'"); return `import { defineMongooseModel } from '#nuxt/mongoose' export const ${name}Schema = defineMongooseModel({ name: '${name}', schema: ${outputObject}, }) `; } function generateApiRoute(action, { model, by }) { const modelName = capitalize(model.name); const operation = { index: `return await ${modelName}Schema.find({})`, create: `return await new ${modelName}Schema(body).save()`, show: `return await ${modelName}Schema.findOne({ ${by}: event.context.params?.${by} })`, put: `return await ${modelName}Schema.findOneAndUpdate({ ${by}: event.context.params?.${by} }, body, { new: true })`, delete: `return await ${modelName}Schema.findOneAndDelete({ ${by}: event.context.params?.${by} })` }[action]; const main = `try { ${operation} } catch (error) { return error }`; return `export default defineEventHandler(async (event) => { ${action === "create" || action === "put" ? `const body = await readBody(event) ${main}` : main} }) `; } function setupDatabaseRPC({}) { return { async readyState() { return mongoose.connection.readyState; }, async createCollection(name) { try { return await mongoose.connection.db.createCollection(name); } catch (error) { return ErrorIT(error); } }, async listCollections() { try { return await mongoose.connection.db.listCollections().toArray(); } catch (error) { return ErrorIT(error); } }, async getCollection(name) { try { return await mongoose.connection.db.collection(name).findOne(); } catch (error) { return ErrorIT(error); } }, async dropCollection(name) { try { return await mongoose.connection.db.dropCollection(name); } catch (error) { return ErrorIT(error); } }, async createDocument(collection, data) { const { _id, ...rest } = data; try { return await mongoose.connection.db.collection(collection).insertOne(rest); } catch (error) { return ErrorIT(error); } }, async countDocuments(collection) { try { return await mongoose.connection.db.collection(collection).countDocuments(); } catch (error) { return ErrorIT(error); } }, async listDocuments(collection, options = { page: 1, limit: 10 }) { const skip = (options.page - 1) * options.limit; const cursor = mongoose.connection.db.collection(collection).find().skip(skip); if (options.limit !== 0) cursor?.limit(options.limit); return await cursor?.toArray(); }, async getDocument(collection, document) { try { return await mongoose.connection.db.collection(collection).findOne({ document }); } catch (error) { return ErrorIT(error); } }, async updateDocument(collection, data) { const { _id, ...rest } = data; try { return await mongoose.connection.db.collection(collection).findOneAndUpdate({ _id: new mongoose.Types.ObjectId(_id) }, { $set: rest }); } catch (error) { return ErrorIT(error); } }, async deleteDocument(collection, id) { try { return await mongoose.connection.db.collection(collection).deleteOne({ _id: new mongoose.Types.ObjectId(id) }); } catch (error) { return ErrorIT(error); } } }; } function ErrorIT(error) { return { error: { message: error?.message, code: error?.code } }; } function setupResourceRPC({ nuxt }) { const config = nuxt.options.runtimeConfig.mongoose; return { async generateResource(collection2, resources) { const singular2 = singularize(collection2.name).toLowerCase(); const plural = pluralize(collection2.name).toLowerCase(); const dbName = capitalize(singular2); if (collection2.fields) { const schemaPath2 = join(config.modelsDir, `${singular2}.schema.ts`); if (!fs.existsSync(schemaPath2)) { fs.ensureDirSync(config.modelsDir); fs.writeFileSync(schemaPath2, generateSchemaFile(dbName, collection2.fields)); } const model = { name: dbName, path: `${singular2}.schema` }; const routeTypes = { index: "index.get.ts", create: "create.post.ts", show: (by) => `[${by}].get.ts`, put: (by) => `[${by}].put.ts`, delete: (by) => `[${by}].delete.ts` }; resources.forEach((route) => { const fileName = typeof routeTypes[route.type] === "function" ? routeTypes[route.type](route.by) : routeTypes[route.type]; const filePath = join(nuxt.options.serverDir, "api", plural, fileName); if (!fs.existsSync(filePath)) { fs.ensureDirSync(join(nuxt.options.serverDir, `api/${plural}`)); const content2 = generateApiRoute(route.type, { model, by: route.by }); fs.writeFileSync(filePath, content2); } }); } const collections = await mongoose.connection.db.listCollections().toArray(); if (!collections.find((c) => c.name === plural)) return await mongoose.connection.db.createCollection(plural); }, async resourceSchema(collection) { const singular = singularize(collection).toLowerCase(); const schemaPath = join(config.modelsDir, `${singular}.schema.ts`); if (fs.existsSync(schemaPath)) { const content = fs.readFileSync(schemaPath, "utf-8").match(/schema: \{(.|\n)*\}/g); if (content) { const schemaString = content[0].replace("schema: ", "").slice(0, -3); const schema = eval(`(${schemaString})`); return schema; } } } }; } function setupRPC(ctx) { mongoose.connect(ctx.options.uri, ctx.options.options); return { getOptions() { return ctx.options; }, ...setupDatabaseRPC(ctx), ...setupResourceRPC(ctx), async reset() { const ws = await ctx.wsServer; ws.send("nuxt-mongoose:reset"); } }; } const CLIENT_PATH = "/__nuxt-mongoose"; const CLIENT_PORT = 3300; const RPC_NAMESPACE = "nuxt-mongoose-rpc"; function setupDevToolsUI(options, resolve, nuxt) { const clientPath = resolve("./client"); const isProductionBuild = existsSync(clientPath); if (isProductionBuild) { nuxt.hook("vite:serverCreated", async (server) => { const sirv = await import('sirv').then((r) => r.default || r); server.middlewares.use( CLIENT_PATH, sirv(clientPath, { dev: true, single: true }) ); }); } else { nuxt.hook("vite:extendConfig", (config) => { config.server = config.server || {}; config.server.proxy = config.server.proxy || {}; config.server.proxy[CLIENT_PATH] = { target: `http://localhost:${CLIENT_PORT}${CLIENT_PATH}`, changeOrigin: true, followRedirects: true, rewrite: (path) => path.replace(CLIENT_PATH, "") }; }); } nuxt.hook("devtools:customTabs", (tabs) => { tabs.push({ name: "nuxt-mongoose", title: "Mongoose", icon: "skill-icons:mongodb", view: { type: "iframe", src: CLIENT_PATH } }); }); const wsServer = useViteWebSocket(nuxt); onDevToolsInitialized(async () => { const rpcFunctions = setupRPC({ options, wsServer, nuxt }); extendServerRpc(RPC_NAMESPACE, rpcFunctions); }); } const module = defineNuxtModule({ meta: { name: "nuxt-mongoose", configKey: "mongoose" }, defaults: { // eslint-disable-next-line n/prefer-global/process uri: process.env.MONGODB_URI, devtools: true, options: {}, modelsDir: "models" }, hooks: { close: () => { mongoose.disconnect(); } }, async setup(options, nuxt) { if (nuxt.options.dev) { $fetch("https://registry.npmjs.org/nuxt-mongoose/latest").then((release) => { if (release.version > version) logger.info(`A new version of Nuxt Mongoose (v${release.version}) is available: https://github.com/arashsheyda/nuxt-mongoose/releases/latest`); }).catch(() => { }); } if (!options.uri) { logger.warn("Missing MongoDB URI. You can set it in your `nuxt.config` or in your `.env` as `MONGODB_URI`"); } const { resolve } = createResolver(import.meta.url); const config = nuxt.options.runtimeConfig; config.mongoose = defu(config.mongoose || {}, { uri: options.uri, options: options.options, devtools: options.devtools, modelsDir: join(nuxt.options.serverDir, options.modelsDir) }); nuxt.hook("nitro:config", (_config) => { _config.alias = _config.alias || {}; _config.externals = defu(typeof _config.externals === "object" ? _config.externals : {}, { inline: [resolve("./runtime")] }); _config.alias["#nuxt/mongoose"] = resolve("./runtime/server/services"); if (_config.imports) { _config.imports.dirs = _config.imports.dirs || []; _config.imports.dirs?.push(config.mongoose.modelsDir); } }); addTemplate({ filename: "types/nuxt-mongoose.d.ts", getContents: () => [ "declare module '#nuxt/mongoose' {", ` const defineMongooseConnection: typeof import('${resolve("./runtime/server/services")}').defineMongooseConnection`, ` const defineMongooseModel: typeof import('${resolve("./runtime/server/services")}').defineMongooseModel`, "}" ].join("\n") }); nuxt.hook("prepare:types", (options2) => { options2.references.push({ path: resolve(nuxt.options.buildDir, "types/nuxt-mongoose.d.ts") }); }); const isDevToolsEnabled = typeof nuxt.options.devtools === "boolean" ? nuxt.options.devtools : nuxt.options.devtools.enabled; if (nuxt.options.dev && isDevToolsEnabled) setupDevToolsUI(options, resolve, nuxt); addServerPlugin(resolve("./runtime/server/plugins/mongoose.db")); logger.success("`nuxt-mongoose` is ready!"); } }); export { module as default };