nuxt-mongoose
Version:
Nuxt 3 module for MongoDB with Mongoose
336 lines (325 loc) • 11.2 kB
JavaScript
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 };