feat: experimental generate resource

This commit is contained in:
arashsheyda
2023-04-23 00:25:11 +03:00
parent a95cb78c6a
commit 5de7715356
17 changed files with 617 additions and 57 deletions

View File

@ -1,4 +1,4 @@
import { addServerPlugin, addTemplate, createResolver, defineNuxtModule, logger } from '@nuxt/kit'
import { addImportsDir, addServerPlugin, addTemplate, createResolver, defineNuxtModule, logger } from '@nuxt/kit'
import { pathExists } from 'fs-extra'
import { tinyws } from 'tinyws'
import { defu } from 'defu'
@ -22,6 +22,8 @@ export default defineNuxtModule<ModuleOptions>({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url)
addImportsDir(resolve('./runtime/composables'))
if (!options.uri)
console.warn('Missing `MONGODB_URI` in `.env`')

View File

@ -0,0 +1,8 @@
import type { mongo } from 'mongoose'
import { connection } from 'mongoose'
export function useMongoose(): { db: mongo.Db } {
return {
db: connection?.db,
}
}

View File

@ -1,39 +1,38 @@
import { logger } from '@nuxt/kit'
import { ObjectId } from 'mongodb'
import { connection as MongooseConnection, connect } from 'mongoose'
import mongoose from 'mongoose'
import type { NuxtDevtoolsServerContext, ServerFunctions } from '../types'
export function setupDatabaseRPC({ nuxt }: NuxtDevtoolsServerContext): any {
// TODO:
connect('mongodb://127.0.0.1:27017/arcane')
mongoose.connect('mongodb://127.0.0.1:27017/arcane')
return {
async createCollection(name: string) {
return await MongooseConnection.db.createCollection(name)
return await mongoose.connection.db.createCollection(name)
},
async listCollections() {
return await MongooseConnection.db.listCollections().toArray()
return await mongoose.connection.db.listCollections().toArray()
},
async getCollection(name: string) {
return MongooseConnection.db.collection(name)
return mongoose.connection.db.collection(name)
},
async dropCollection(name: string) {
return await MongooseConnection.db.collection(name).drop()
return await mongoose.connection.db.collection(name).drop()
},
async createDocument(collection: string, data: any) {
return await MongooseConnection.db.collection(collection).insertOne(data)
return await mongoose.connection.db.collection(collection).insertOne(data)
},
async listDocuments(collection: string) {
return await MongooseConnection.db.collection(collection).find().toArray()
return await mongoose.connection.db.collection(collection).find().toArray()
},
async getDocument(collection: string, document: {}) {
return await MongooseConnection.db.collection(collection).findOne({ document })
return await mongoose.connection.db.collection(collection).findOne({ document })
},
async updateDocument(collection: string, data: any) {
const { _id, ...rest } = data
try {
return await MongooseConnection.db.collection(collection).findOneAndUpdate({ _id: new ObjectId(_id) }, { $set: rest })
return await mongoose.connection.db.collection(collection).findOneAndUpdate({ _id: new mongoose.Types.ObjectId(_id) }, { $set: rest })
}
catch (error) {
logger.log(error)
@ -41,7 +40,7 @@ export function setupDatabaseRPC({ nuxt }: NuxtDevtoolsServerContext): any {
}
},
async deleteDocument(collection: string, id: string) {
return await MongooseConnection.db.collection(collection).deleteOne({ _id: new ObjectId(id) })
return await mongoose.connection.db.collection(collection).deleteOne({ _id: new mongoose.Types.ObjectId(id) })
},
} satisfies Partial<ServerFunctions>
}

View File

@ -8,6 +8,7 @@ import { parse, stringify } from 'flatted'
import type { Nuxt } from 'nuxt/schema'
import type { ClientFunctions, ModuleOptions, NuxtDevtoolsServerContext, ServerFunctions } from '../types'
import { setupDatabaseRPC } from './database'
import { setupResourceRPC } from './resource'
export function setupRPC(nuxt: Nuxt, options: ModuleOptions): any {
const serverFunctions = {} as ServerFunctions
@ -63,6 +64,7 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions): any {
Object.assign(serverFunctions, {
...setupDatabaseRPC(ctx),
...setupResourceRPC(ctx),
} satisfies Partial<ServerFunctions>)
const wsClients = new Set<WebSocket>()

View File

@ -0,0 +1,77 @@
import fs from 'fs-extra'
import { resolve } from 'pathe'
import mongoose from 'mongoose'
import type { Collection, NuxtDevtoolsServerContext, Resource, ServerFunctions } from '../types'
import { generateApiRoute, generateSchemaFile } from '../utils/schematics'
import { capitalize, pluralize, singularize } from '../utils/formatting'
export function setupResourceRPC({ nuxt }: NuxtDevtoolsServerContext): any {
return {
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) {
if (!fs.existsSync(resolve(nuxt.options.serverDir, 'models', `${singular}.schema.ts`))) {
fs.ensureDirSync(resolve(nuxt.options.serverDir, 'models'))
fs.writeFileSync(
resolve(nuxt.options.serverDir, 'models', `${singular}.schema.ts`),
generateSchemaFile(dbName, collection.fields),
)
}
const model = { name: dbName, path: `${singular}.schema` }
fs.ensureDirSync(resolve(nuxt.options.serverDir, `api/${plural}`))
// create resources
// TODO: fix this
resources.forEach((route: any) => {
let fileName = ''
if (route.type === 'index')
fileName = 'index.get.ts'
if (route.type === 'create')
fileName = 'create.post.ts'
if (route.type === 'show')
fileName = `[_${route.by}].get.ts`.replace('_', '')
if (route.type === 'put')
fileName = `[_${route.by}].put.ts`.replace('_', '')
if (route.type === 'delete')
fileName = `[_${route.by}].delete.ts`.replace('_', '')
if (!fs.existsSync(resolve(nuxt.options.serverDir, `api/${plural}`, fileName))) {
const content = generateApiRoute(route.type, { model, by: route.by })
fs.writeFileSync(resolve(nuxt.options.serverDir, 'api', plural, fileName), content)
}
})
}
// create collection if not exists
if (!mongoose.connection.modelNames().includes(dbName))
await mongoose.connection.db.createCollection(plural)
// create rows and columns
},
async resourceSchema(collection: string) {
// get schema file if exists
const singular = singularize(collection).toLowerCase()
if (fs.existsSync(resolve(nuxt.options.serverDir, 'models', `${singular}.schema.ts`))) {
const schemaPath = resolve(nuxt.options.serverDir, 'models', `${singular}.schema.ts`)
const content = fs.readFileSync(schemaPath, 'utf-8').match(/schema: \{(.|\n)*\}/g)
if (content) {
const schemaString = content[0].replace('schema: ', '').slice(0, -3)
// eslint-disable-next-line no-eval
const schema = eval(`(${schemaString})`)
return schema
}
}
},
} satisfies Partial<ServerFunctions>
}

View File

@ -1,18 +1,32 @@
export interface ServerFunctions {
// collections
// Database - collections
createCollection(name: string): Promise<any>
listCollections(): Promise<any>
getCollection(name: string): Promise<any>
dropCollection(name: string): Promise<any>
// documents
// Database - documents
createDocument(collection: string, data: any): Promise<any>
listDocuments(collection: string): 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?: {}[]
}
export interface Resource {
type: string
by?: string
}

22
src/utils/formatting.ts Normal file
View File

@ -0,0 +1,22 @@
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)
}

49
src/utils/schematics.ts Normal file
View File

@ -0,0 +1,49 @@
import { capitalize } from './formatting'
export function generateSchemaFile(name: string, fields: any) {
name = capitalize(name)
// TODO: fix spacing
const outputObject = JSON.stringify(
fields.reduce((acc: any, curr: any) => {
const { name, ...rest } = curr
acc[name] = 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},
})
`
}
export function generateApiRoute(action: string, { model, by }: { model: { name: string; path: string }; by?: string }) {
const modelName = capitalize(model.name)
const schemaImport = `import { ${modelName}Schema } from '../../models/${model.path}'\n\n`
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 `${schemaImport}export default defineEventHandler(async (event) => {
${(action === 'create' || action === 'put') ? `const body = await readBody(event)\n ${main}` : main}
})
`
}