feat: version 1.0.0 (#21)

This commit is contained in:
Arash
2023-09-20 19:47:07 +03:00
committed by GitHub
parent 033380e051
commit 431d3784fe
42 changed files with 3654 additions and 3219 deletions

View File

@ -1,3 +1,3 @@
export const PATH = '/__nuxt_mongoose__'
export const PATH_CLIENT = `${PATH}/client`
export const WS_EVENT_NAME = 'nuxt:devtools:mongoose:rpc'
export const CLIENT_PATH = '/__nuxt-mongoose'
export const CLIENT_PORT = 3300
export const RPC_NAMESPACE = 'nuxt-mongoose-rpc'

56
src/devtools.ts Normal file
View File

@ -0,0 +1,56 @@
import { existsSync } from 'node:fs'
import type { Nuxt } from 'nuxt/schema'
import type { Resolver } from '@nuxt/kit'
import { extendServerRpc, onDevToolsInitialized } from '@nuxt/devtools-kit'
import type { ClientFunctions, ServerFunctions } from './types'
import type { ModuleOptions } from './module'
import { useViteWebSocket } from './utils'
import { setupRPC } from './rpc'
import { CLIENT_PATH, CLIENT_PORT, RPC_NAMESPACE } from './constants'
export function setupDevToolsUI(options: ModuleOptions, resolve: Resolver['resolve'], nuxt: 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<ClientFunctions, ServerFunctions>(RPC_NAMESPACE, rpcFunctions)
})
}

View File

@ -1,25 +1,46 @@
import {
addImportsDir,
addServerPlugin,
addTemplate,
addVitePlugin,
createResolver,
defineNuxtModule,
logger,
} from '@nuxt/kit'
import { pathExists } from 'fs-extra'
import type { ConnectOptions } from 'mongoose'
import defu from 'defu'
import { join } from 'pathe'
import { defu } from 'defu'
import sirv from 'sirv'
import { $fetch } from 'ofetch'
import { version } from '../package.json'
import { setupDevToolsUI } from './devtools'
import { PATH_CLIENT } from './constants'
import type { ModuleOptions } from './types'
import { setupRPC } from './server-rpc'
export type { ModuleOptions }
export interface ModuleOptions {
/**
* The MongoDB URI connection
*
* @default process.env.MONGODB_URI
*
*/
uri: string | undefined
/**
* Nuxt DevTools
*
* @default true
*
*/
devtools: boolean
/**
* Mongoose Connections
*
* @default {}
*/
options?: ConnectOptions
/**
* Models Directory for auto-import
*
* @default 'models'
*
*/
modelsDir?: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
@ -27,15 +48,13 @@ export default defineNuxtModule<ModuleOptions>({
configKey: 'mongoose',
},
defaults: {
// eslint-disable-next-line n/prefer-global/process
uri: process.env.MONGODB_URI as string,
devtools: true,
options: {},
modelsDir: 'models',
},
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
const runtimeConfig = nuxt.options.runtimeConfig as any
async setup(options, nuxt) {
if (nuxt.options.dev) {
$fetch('https://registry.npmjs.org/nuxt-mongoose/latest').then((release) => {
if (release.version > version)
@ -43,62 +62,35 @@ export default defineNuxtModule<ModuleOptions>({
}).catch(() => {})
}
addImportsDir(resolve('./runtime/composables'))
if (!options.uri) {
logger.warn('Missing `MONGODB_URI` in `.env`')
logger.warn('Missing MongoDB URI. You can set it in your `nuxt.config` or in your `.env` as `MONGODB_URI`')
return
}
// Runtime Config
runtimeConfig.mongoose = defu(runtimeConfig.mongoose || {}, {
const { resolve } = createResolver(import.meta.url)
const config = nuxt.options.runtimeConfig as any
config.mongoose = defu(config.mongoose || {}, {
uri: options.uri,
options: options.options,
devtools: options.devtools,
modelsDir: options.modelsDir,
})
// Setup devtools UI
const distResolve = (p: string) => {
const cwd = resolve('.')
if (cwd.endsWith('/dist'))
return resolve(p)
return resolve(`../dist/${p}`)
}
const clientPath = distResolve('./client')
const { vitePlugin } = setupRPC(nuxt, options)
addVitePlugin(vitePlugin)
nuxt.hook('vite:serverCreated', async (server) => {
if (await pathExists(clientPath))
server.middlewares.use(PATH_CLIENT, sirv(clientPath, { dev: true, single: true }))
})
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore runtime type
nuxt.hook('devtools:customTabs', (iframeTabs) => {
iframeTabs.push({
name: 'mongoose',
title: 'Mongoose',
icon: 'skill-icons:mongodb',
view: {
type: 'iframe',
src: PATH_CLIENT,
},
})
modelsDir: join(nuxt.options.serverDir, options.modelsDir!),
})
// virtual imports
nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.alias = nitroConfig.alias || {}
nuxt.hook('nitro:config', (_config) => {
_config.alias = _config.alias || {}
// Inline module runtime in Nitro bundle
nitroConfig.externals = defu(typeof nitroConfig.externals === 'object' ? nitroConfig.externals : {}, {
_config.externals = defu(typeof _config.externals === 'object' ? _config.externals : {}, {
inline: [resolve('./runtime')],
})
nitroConfig.alias['#nuxt/mongoose'] = resolve('./runtime/server/services')
_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({
@ -115,15 +107,9 @@ export default defineNuxtModule<ModuleOptions>({
options.references.push({ path: resolve(nuxt.options.buildDir, 'types/nuxt-mongoose.d.ts') })
})
// Nitro auto imports
nuxt.hook('nitro:config', (_nitroConfig) => {
if (_nitroConfig.imports) {
_nitroConfig.imports.dirs = _nitroConfig.imports.dirs || []
_nitroConfig.imports.dirs?.push(
join(nuxt.options.serverDir, runtimeConfig.mongoose.modelsDir),
)
}
})
const isDevToolsEnabled = typeof nuxt.options.devtools === 'boolean' ? nuxt.options.devtools : nuxt.options.devtools.enabled
if (nuxt.options.dev && isDevToolsEnabled)
setupDevToolsUI(options, resolve, nuxt)
// Add server-plugin for database connection
addServerPlugin(resolve('./runtime/server/plugins/mongoose.db'))

View File

@ -1,9 +1,8 @@
import mongoose from 'mongoose'
import type { NuxtDevtoolsServerContext, ServerFunctions } from '../types'
export function setupDatabaseRPC({ options }: NuxtDevtoolsServerContext): any {
mongoose.connect(options.uri, options.options)
import type { DevtoolsServerContext, ServerFunctions } from '../types'
// eslint-disable-next-line no-empty-pattern
export function setupDatabaseRPC({}: DevtoolsServerContext) {
return {
async readyState() {
return mongoose.connection.readyState
@ -62,8 +61,8 @@ export function setupDatabaseRPC({ options }: NuxtDevtoolsServerContext): any {
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()
cursor?.limit(options.limit)
return await cursor?.toArray()
},
async getDocument(collection: string, document: any) {
try {

23
src/rpc/index.ts Normal file
View File

@ -0,0 +1,23 @@
import mongoose from 'mongoose'
import type { DevtoolsServerContext, ServerFunctions } from '../types'
import { setupDatabaseRPC } from './database'
import { setupResourceRPC } from './resource'
export function setupRPC(ctx: DevtoolsServerContext): ServerFunctions {
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')
},
}
}

View File

@ -1,23 +1,22 @@
import fs from 'fs-extra'
import { resolve } from 'pathe'
import type { Collection, NuxtDevtoolsServerContext, Resource, ServerFunctions } from '../types'
import { generateApiRoute, generateSchemaFile } from '../utils/schematics'
import { capitalize, pluralize, singularize } from '../utils/formatting'
import { join } from 'pathe'
import mongoose from 'mongoose'
import type { Collection, DevtoolsServerContext, Resource, ServerFunctions } from '../types'
import { capitalize, generateApiRoute, generateSchemaFile, pluralize, singularize } from '../utils'
export function setupResourceRPC({ nuxt, rpc }: NuxtDevtoolsServerContext): any {
const runtimeConfig = nuxt.options.runtimeConfig as any
export function setupResourceRPC({ nuxt }: DevtoolsServerContext): any {
const config = nuxt.options.runtimeConfig.mongoose
return {
// TODO: maybe separate functions
async generateResource(collection: Collection, resources: Resource[]) {
const singular = singularize(collection.name).toLowerCase()
const plural = pluralize(collection.name).toLowerCase()
const dbName = capitalize(singular)
if (collection.fields) {
const schemaPath = resolve(nuxt.options.serverDir, runtimeConfig.mongoose.modelsDir, `${singular}.schema.ts`)
const schemaPath = join(config.modelsDir, `${singular}.schema.ts`)
if (!fs.existsSync(schemaPath)) {
fs.ensureDirSync(resolve(nuxt.options.serverDir, runtimeConfig.mongoose.modelsDir))
fs.ensureDirSync(config.modelsDir)
fs.writeFileSync(schemaPath, generateSchemaFile(dbName, collection.fields))
}
@ -36,9 +35,9 @@ export function setupResourceRPC({ nuxt, rpc }: NuxtDevtoolsServerContext): any
? (routeTypes[route.type] as any)(route.by)
: routeTypes[route.type]
const filePath = resolve(nuxt.options.serverDir, 'api', plural, fileName)
const filePath = join(nuxt.options.serverDir, 'api', plural, fileName)
if (!fs.existsSync(filePath)) {
fs.ensureDirSync(resolve(nuxt.options.serverDir, `api/${plural}`))
fs.ensureDirSync(join(nuxt.options.serverDir, `api/${plural}`))
const content = generateApiRoute(route.type, { model, by: route.by })
fs.writeFileSync(filePath, content)
}
@ -46,14 +45,13 @@ export function setupResourceRPC({ nuxt, rpc }: NuxtDevtoolsServerContext): any
}
// create collection if not exists
const collections = await rpc.functions.listCollections()
const collections = await mongoose.connection.db.listCollections().toArray()
if (!collections.find((c: any) => c.name === plural))
await rpc.functions.createCollection(plural)
return await mongoose.connection.db.createCollection(plural)
},
async resourceSchema(collection: string) {
// TODO: use magicast
const singular = singularize(collection).toLowerCase()
const schemaPath = resolve(nuxt.options.serverDir, runtimeConfig.mongoose.modelsDir, `${singular}.schema.ts`)
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) {

View File

@ -1,8 +1,8 @@
/**
* Due to an upstream bug in Nuxt 3 we need to stub the plugin here, track:https://github.com/nuxt/nuxt/issues/18556
* */
*/
import type { NitroApp } from 'nitropack'
import { defineMongooseConnection } from '../services/mongoose'
import { defineMongooseConnection } from '../services'
type NitroAppPlugin = (nitro: NitroApp) => void

View File

@ -1 +1,50 @@
export { defineMongooseConnection, defineMongooseModel } from './mongoose'
import { logger } from '@nuxt/kit'
import mongoose from 'mongoose'
import type { ConnectOptions, Model, SchemaDefinition, SchemaOptions } from 'mongoose'
import { useRuntimeConfig } from '#imports'
export async function defineMongooseConnection({ uri, options }: { uri?: string; options?: ConnectOptions } = {}): Promise<void> {
// TODO: types
const config = useRuntimeConfig().mongoose
const mongooseUri = uri || config.uri
const mongooseOptions = options || config.options
try {
await mongoose.connect(mongooseUri, { ...mongooseOptions })
logger.success('Connected to `MongoDB`')
}
catch (err) {
logger.error('Error connecting to `MongoDB`', err)
}
}
export function defineMongooseModel<T>(
nameOrOptions: string | {
name: string
schema: SchemaDefinition
options?: SchemaOptions
hooks?: (schema: mongoose.Schema<T>) => void
},
schema?: SchemaDefinition,
options?: SchemaOptions,
hooks?: (schema: mongoose.Schema<T>) => void,
): Model<T> {
let name: string
if (typeof nameOrOptions === 'string') {
name = nameOrOptions
}
else {
name = nameOrOptions.name
schema = nameOrOptions.schema
options = nameOrOptions.options
hooks = nameOrOptions.hooks
}
const newSchema = new mongoose.Schema<T>(schema, options as any)
if (hooks)
hooks(newSchema)
return mongoose.model<T>(name, newSchema)
}

View File

@ -1,49 +0,0 @@
import type { ConnectOptions, Model, SchemaDefinition, SchemaOptions } from 'mongoose'
import mongoose from 'mongoose'
import { logger } from '@nuxt/kit'
import { useRuntimeConfig } from '#imports'
export async function defineMongooseConnection({ uri, options }: { uri?: string; options?: ConnectOptions } = {}): Promise<void> {
const config = useRuntimeConfig().mongoose
const mongooseUri = uri || config.uri
const mongooseOptions = options || config.options
try {
await mongoose.connect(mongooseUri, { ...mongooseOptions })
logger.success('Connected to MongoDB database')
}
catch (err) {
logger.error('Error connecting to MongoDB database', err)
}
}
export function defineMongooseModel<T>(
nameOrOptions: string | {
name: string
schema: SchemaDefinition
options?: SchemaOptions
hooks?: (schema: mongoose.Schema<T>) => void
},
schema?: SchemaDefinition,
options?: SchemaOptions,
hooks?: (schema: mongoose.Schema<T>) => void,
): Model<T> {
let name: string
if (typeof nameOrOptions === 'string') {
name = nameOrOptions
}
else {
name = nameOrOptions.name
schema = nameOrOptions.schema
options = nameOrOptions.options
hooks = nameOrOptions.hooks
}
const newSchema = new mongoose.Schema<T>(schema, options as any)
if (hooks)
hooks(newSchema)
return mongoose.model<T>(name, newSchema)
}

View File

@ -1,117 +0,0 @@
import type { WebSocket } from 'ws'
import { createBirpcGroup } from 'birpc'
import type { ChannelOptions } from 'birpc'
import { parse, stringify } from 'flatted'
import type { Plugin } from 'vite'
import type { Nuxt } from 'nuxt/schema'
import type { ClientFunctions, ModuleOptions, NuxtDevtoolsServerContext, ServerFunctions } from '../types'
import { WS_EVENT_NAME } from '../constants'
import { setupDatabaseRPC } from './database'
import { setupResourceRPC } from './resource'
export function setupRPC(nuxt: Nuxt, options: ModuleOptions): any {
const serverFunctions = {} as ServerFunctions
const extendedRpcMap = new Map<string, any>()
const rpc = createBirpcGroup<ClientFunctions, ServerFunctions>(
serverFunctions,
[],
{
resolver: (name, fn) => {
if (fn)
return fn
if (!name.includes(':'))
return
const [namespace, fnName] = name.split(':')
return extendedRpcMap.get(namespace)?.[fnName]
},
onError(error, name) {
console.error(`[nuxt-devtools] RPC error on executing "${name}":`, error)
},
},
)
function refresh(event: keyof ServerFunctions) {
rpc.broadcast.refresh.asEvent(event)
}
function extendServerRpc(namespace: string, functions: any): any {
extendedRpcMap.set(namespace, functions)
return {
broadcast: new Proxy({}, {
get: (_, key) => {
if (typeof key !== 'string')
return
return (rpc.broadcast as any)[`${namespace}:${key}`]
},
}),
}
}
const ctx: NuxtDevtoolsServerContext = {
nuxt,
options,
rpc,
refresh,
extendServerRpc,
}
// @ts-expect-error untyped
nuxt.devtools = ctx
Object.assign(serverFunctions, {
...setupDatabaseRPC(ctx),
...setupResourceRPC(ctx),
} satisfies Partial<ServerFunctions>)
const wsClients = new Set<WebSocket>()
const vitePlugin: Plugin = {
name: 'nuxt:devtools:rpc',
configureServer(server) {
server.ws.on('connection', (ws) => {
wsClients.add(ws)
const channel: ChannelOptions = {
post: d => ws.send(JSON.stringify({
type: 'custom',
event: WS_EVENT_NAME,
data: d,
})),
on: (fn) => {
ws.on('message', (e) => {
try {
const data = JSON.parse(String(e)) || {}
if (data.type === 'custom' && data.event === WS_EVENT_NAME) {
// console.log(data.data)
fn(data.data)
}
}
catch {}
})
},
serialize: stringify,
deserialize: parse,
}
rpc.updateChannels((c) => {
c.push(channel)
})
ws.on('close', () => {
wsClients.delete(ws)
rpc.updateChannels((c) => {
const index = c.indexOf(channel)
if (index >= 0)
c.splice(index, 1)
})
})
})
},
}
return {
vitePlugin,
...ctx,
}
}

View File

@ -1,3 +1,47 @@
export * from './rpc'
export * from './server-ctx'
export * from './module-options'
import type { Nuxt } from 'nuxt/schema'
import type { WebSocketServer } from 'vite'
import type { ModuleOptions } from '../module'
export interface ServerFunctions {
getOptions(): ModuleOptions
// Database - collections
readyState(): Promise<any>
createCollection(name: string): Promise<any>
listCollections(): Promise<any>
getCollection(name: string): Promise<any>
dropCollection(name: string): Promise<any>
// Database - documents
createDocument(collection: string, data: any): Promise<any>
countDocuments(collection: string): Promise<any>
listDocuments(collection: string, options: any): Promise<any>
getDocument(collection: string, id: string): Promise<any>
updateDocument(collection: string, data: any): Promise<any>
deleteDocument(collection: string, id: string): Promise<any>
// Resource - api-routes & models
generateResource(collection: Collection, resources: Resource[]): Promise<any>
resourceSchema(collection: string): Promise<any>
reset(): void
}
export interface ClientFunctions {
}
export interface DevtoolsServerContext {
nuxt: Nuxt
options: ModuleOptions
wsServer: Promise<WebSocketServer>
}
export interface Collection {
name: string
fields?: object[]
}
export interface Resource {
type: 'index' | 'create' | 'show' | 'put' | 'delete'
by?: string
}

View File

@ -1,8 +0,0 @@
import type { ConnectOptions } from 'mongoose'
export interface ModuleOptions {
uri: string
devtools: boolean
options?: ConnectOptions
modelsDir?: string
}

View File

@ -1,34 +0,0 @@
export interface ServerFunctions {
// Database - collections
readyState(): Promise<any>
createCollection(name: string): Promise<any>
listCollections(): Promise<any>
getCollection(name: string): Promise<any>
dropCollection(name: string): Promise<any>
// Database - documents
createDocument(collection: string, data: any): Promise<any>
countDocuments(collection: string): Promise<any>
listDocuments(collection: string, options: any): Promise<any>
getDocument(collection: string, id: string): Promise<any>
updateDocument(collection: string, data: any): Promise<any>
deleteDocument(collection: string, id: string): Promise<any>
// Resource - api-routes & models
generateResource(collection: Collection, resources: Resource[]): Promise<any>
resourceSchema(collection: string): Promise<any>
}
export interface ClientFunctions {
refresh(type: string): void
}
export interface Collection {
name: string
fields?: object[]
}
export interface Resource {
type: 'index' | 'create' | 'show' | 'put' | 'delete'
by?: string
}

View File

@ -1,15 +0,0 @@
import type { BirpcGroup } from 'birpc'
import type { Nuxt } from 'nuxt/schema'
import type { ClientFunctions, ServerFunctions } from './rpc'
import type { ModuleOptions } from './module-options'
export interface NuxtDevtoolsServerContext {
nuxt: Nuxt
options: ModuleOptions
rpc: BirpcGroup<ClientFunctions, ServerFunctions>
refresh: (event: keyof ServerFunctions) => void
extendServerRpc: <ClientFunctions = object, ServerFunctions = object>(name: string, functions: ServerFunctions) => BirpcGroup<ClientFunctions, ServerFunctions>
}

View File

@ -1,22 +0,0 @@
import plrz from 'pluralize'
export function normalizeToKebabOrSnakeCase(str: string) {
const STRING_DASHERIZE_REGEXP = /\s/g
const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g
return str
.replace(STRING_DECAMELIZE_REGEXP, '$1-$2')
.toLowerCase()
.replace(STRING_DASHERIZE_REGEXP, '-')
}
export function pluralize(str: string) {
return plrz.plural(str)
}
export function singularize(str: string) {
return plrz.singular(str)
}
export function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}

View File

@ -1,4 +1,35 @@
import { capitalize } from './formatting'
import type { WebSocketServer } from 'vite'
import type { Nuxt } from 'nuxt/schema'
import plrz from 'pluralize'
export function useViteWebSocket(nuxt: Nuxt) {
return new Promise<WebSocketServer>((_resolve) => {
nuxt.hooks.hook('vite:serverCreated', (viteServer) => {
_resolve(viteServer.ws)
})
})
}
export function normalizeToKebabOrSnakeCase(str: string) {
const STRING_DASHERIZE_REGEXP = /\s/g
const STRING_DECAMELIZE_REGEXP = /([a-z\d])([A-Z])/g
return str
.replace(STRING_DECAMELIZE_REGEXP, '$1-$2')
.toLowerCase()
.replace(STRING_DASHERIZE_REGEXP, '-')
}
export function pluralize(str: string) {
return plrz.plural(str)
}
export function singularize(str: string) {
return plrz.singular(str)
}
export function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function generateSchemaFile(name: string, fields: any) {
name = capitalize(name)