From 2e39d5072e1c6308b8cbe4c5a46268b9457cfd88 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Sun, 14 Jul 2024 16:03:45 +0800 Subject: [PATCH 1/3] feat: split app from framework: mostly working --- bun.lockb | Bin 206650 -> 207880 bytes bunfig.toml | 2 + core/package.json | 65 ++++++++++++++++++ {src => core/src}/config/config.ts | 0 {src => core/src}/config/database.ts | 0 .../src}/config/excluded-operations.ts | 0 {src => core/src}/config/logger.ts | 0 .../src}/event/kafka-event-service.ts | 4 +- core/src/index.ts | 4 ++ {src => core/src}/middleware/auth.ts | 0 .../src}/plugins/function-registry.ts | 0 {src => core/src}/plugins/global-context.ts | 0 {src => core/src}/plugins/plugin-interface.ts | 0 {src => core/src}/plugins/plugin-loader.ts | 38 +++++----- {src => core/src}/plugins/plugin.ts | 0 {src => core/src}/plugins/plugins-list.ts | 0 {src => core/src}/rbac.ts | 0 {src => core/src}/sanitize-log.ts | 0 src/index.ts => core/src/server.ts | 30 ++++---- core/src/shared.ts | 34 +++++++++ {src => core/src}/types/express.d.ts | 0 {src => core/src}/types/jwt-payload.d.ts | 0 .../src}/utils/introspection-check.ts | 0 {src => core/src}/utils/should-bypass-auth.ts | 0 {src => core/src}/worker.ts | 0 core/tsconfig.json | 10 +++ example-app/.env.example | 10 +++ example-app/package.json | 18 +++++ example-app/src/index.ts | 40 +++++++++++ .../src}/plugins/auth-plugin/bootstrap.ts | 0 .../src}/plugins/auth-plugin/index.ts | 12 ++-- .../src}/plugins/auth-plugin/models/user.ts | 0 .../auth-plugin/resolvers/auth-resolver.ts | 4 +- .../auth-plugin/services/user-service.test.ts | 0 .../auth-plugin/services/user-service.ts | 2 +- .../src}/plugins/cart-plugin/index.ts | 0 .../src}/plugins/cart-plugin/models/cart.ts | 0 .../cart-plugin/resolvers/cart-resolver.ts | 0 .../resolvers/inputs/item-input.ts | 0 .../plugins/cart-plugin/services/index.ts | 0 .../src}/plugins/discount-plugin/index.ts | 0 .../src}/plugins/sample-plugin/index.ts | 12 ++-- .../plugins/sample-plugin/models/sample.ts | 0 .../resolvers/sample-resolver.ts | 2 +- .../sample-plugin/services/sample-service.ts | 4 +- .../sample-plugin/services/sample-worker.ts | 0 example-app/tsconfig.json | 10 +++ package.json | 62 +++-------------- src/shared.ts | 25 ------- tsconfig.base.json | 27 ++++++++ 50 files changed, 285 insertions(+), 130 deletions(-) create mode 100644 bunfig.toml create mode 100644 core/package.json rename {src => core/src}/config/config.ts (100%) rename {src => core/src}/config/database.ts (100%) rename {src => core/src}/config/excluded-operations.ts (100%) rename {src => core/src}/config/logger.ts (100%) rename {src => core/src}/event/kafka-event-service.ts (96%) create mode 100644 core/src/index.ts rename {src => core/src}/middleware/auth.ts (100%) rename {src => core/src}/plugins/function-registry.ts (100%) rename {src => core/src}/plugins/global-context.ts (100%) rename {src => core/src}/plugins/plugin-interface.ts (100%) rename {src => core/src}/plugins/plugin-loader.ts (81%) rename {src => core/src}/plugins/plugin.ts (100%) rename {src => core/src}/plugins/plugins-list.ts (100%) rename {src => core/src}/rbac.ts (100%) rename {src => core/src}/sanitize-log.ts (100%) rename src/index.ts => core/src/server.ts (81%) create mode 100644 core/src/shared.ts rename {src => core/src}/types/express.d.ts (100%) rename {src => core/src}/types/jwt-payload.d.ts (100%) rename {src => core/src}/utils/introspection-check.ts (100%) rename {src => core/src}/utils/should-bypass-auth.ts (100%) rename {src => core/src}/worker.ts (100%) create mode 100644 core/tsconfig.json create mode 100644 example-app/.env.example create mode 100644 example-app/package.json create mode 100644 example-app/src/index.ts rename {src => example-app/src}/plugins/auth-plugin/bootstrap.ts (100%) rename {src => example-app/src}/plugins/auth-plugin/index.ts (52%) rename {src => example-app/src}/plugins/auth-plugin/models/user.ts (100%) rename {src => example-app/src}/plugins/auth-plugin/resolvers/auth-resolver.ts (94%) rename {src => example-app/src}/plugins/auth-plugin/services/user-service.test.ts (100%) rename {src => example-app/src}/plugins/auth-plugin/services/user-service.ts (90%) rename {src => example-app/src}/plugins/cart-plugin/index.ts (100%) rename {src => example-app/src}/plugins/cart-plugin/models/cart.ts (100%) rename {src => example-app/src}/plugins/cart-plugin/resolvers/cart-resolver.ts (100%) rename {src => example-app/src}/plugins/cart-plugin/resolvers/inputs/item-input.ts (100%) rename {src => example-app/src}/plugins/cart-plugin/services/index.ts (100%) rename {src => example-app/src}/plugins/discount-plugin/index.ts (100%) rename {src => example-app/src}/plugins/sample-plugin/index.ts (79%) rename {src => example-app/src}/plugins/sample-plugin/models/sample.ts (100%) rename {src => example-app/src}/plugins/sample-plugin/resolvers/sample-resolver.ts (90%) rename {src => example-app/src}/plugins/sample-plugin/services/sample-service.ts (85%) rename {src => example-app/src}/plugins/sample-plugin/services/sample-worker.ts (100%) create mode 100644 example-app/tsconfig.json delete mode 100644 src/shared.ts create mode 100644 tsconfig.base.json diff --git a/bun.lockb b/bun.lockb index 482f7591c2223843957c866db050eb5640dfa8f7..5202f1e9e76b0fa9929669355ba67991705d55eb 100755 GIT binary patch delta 42003 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 a(mb0L5|4Ss;O8-9FtAe!<75d#D*pvAI0Wzj delta 41353 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 oVg`<6vDBF0et}18@1Mc2$OM^j!e0iF { + const pluginPath = join(pluginDir, 'index.js'); + if (!statSync(pluginDir).isDirectory()) return; try { - const plugin: Plugin = require(`./${pluginName}`).default; + const { default: plugin } = await import(pluginPath); if (!plugin) { - throw new Error(`Plugin in directory ${pluginName} does not have a default export`); + throw new Error(`Plugin in directory ${pluginDir} does not have a default export`); } logger.info(`Loaded plugin: ${plugin.name} of type ${plugin.type}`, loggerCtx); this.plugins.push(plugin); plugin.register?.(Container, this.context); logger.debug(`Registered plugin: ${plugin.name}`, loggerCtx); } catch (error) { - console.error(`Failed to load plugin from directory ${pluginName}:`, error); + console.error(`Failed to load plugin from directory ${pluginDir}:`, error); } } @@ -105,7 +107,7 @@ class PluginLoader { return await buildSchema({ resolvers: allResolvers as unknown as NonEmptyArray, container: Container, - emitSchemaFile: path.resolve(__dirname, '../../schema.graphql'), + emitSchemaFile: join(__dirname, '../../schema.graphql'), }); } catch (error) { logger.error(`Error building schema: ${error}`, loggerCtx); @@ -132,7 +134,7 @@ class PluginLoader { queueEvents.on('failed', (job, err) => { const errorMessage = this.extractErrorMessage(err); - logger.error(`Job ${job.jobId} in queue ${queueName} failed with error: ${errorMessage}`, loggerCtx);; + logger.error(`Job ${job.jobId} in queue ${queueName} failed with error: ${errorMessage}`, loggerCtx); }); this.queues[queueName] = queue; diff --git a/src/plugins/plugin.ts b/core/src/plugins/plugin.ts similarity index 100% rename from src/plugins/plugin.ts rename to core/src/plugins/plugin.ts diff --git a/src/plugins/plugins-list.ts b/core/src/plugins/plugins-list.ts similarity index 100% rename from src/plugins/plugins-list.ts rename to core/src/plugins/plugins-list.ts diff --git a/src/rbac.ts b/core/src/rbac.ts similarity index 100% rename from src/rbac.ts rename to core/src/rbac.ts diff --git a/src/sanitize-log.ts b/core/src/sanitize-log.ts similarity index 100% rename from src/sanitize-log.ts rename to core/src/sanitize-log.ts diff --git a/src/index.ts b/core/src/server.ts similarity index 81% rename from src/index.ts rename to core/src/server.ts index 77d4e88..25bb8a8 100644 --- a/src/index.ts +++ b/core/src/server.ts @@ -1,18 +1,18 @@ import 'reflect-metadata'; import express, { type Application, type Request, type Response, type NextFunction } from 'express'; import { ApolloServer } from 'apollo-server-express'; -import env from './config/config'; -import logger from './config/logger'; -import { authenticate } from './middleware/auth'; -import { isIntrospectionQuery } from './utils/introspection-check'; -import { shouldBypassAuth } from './utils/should-bypass-auth'; -import sanitizeLog from './sanitize-log'; -import { initializeSharedResources } from './shared'; -import { startWorker } from './worker'; -import { getEnforcer } from './rbac.ts'; -import type PluginLoader from './plugins/plugin-loader.ts'; +import env from './config/config.js'; +import logger from './config/logger.js'; +import { authenticate } from './middleware/auth.js'; +import { isIntrospectionQuery } from './utils/introspection-check.js'; +import { shouldBypassAuth } from './utils/should-bypass-auth.js'; +import sanitizeLog from './sanitize-log.js'; +import { initializeSharedResources } from './shared.js'; +import { startWorker } from './worker.js'; +import { getEnforcer } from './rbac.js'; +import type PluginLoader from './plugins/plugin-loader.js'; -const loggerCtx = { context: 'index' }; +const loggerCtx = { context: 'server' }; async function startServer(pluginLoader: PluginLoader) { try { @@ -75,15 +75,15 @@ async function startServer(pluginLoader: PluginLoader) { const port = env.PORT; app.listen(port, () => { - logger.info(`Server is running at http://localhost:${port}${server.graphqlPath}`, { context: 'index' }); + logger.info(`Server is running at http://localhost:${port}${server.graphqlPath}`, { context: 'server' }); }); } catch (error) { logger.error('Failed to start server:', error, loggerCtx); } } -async function startApp() { - const pluginLoader = await initializeSharedResources(); +async function startApp(pluginDirs: string[]) { + const pluginLoader = await initializeSharedResources(pluginDirs); switch (env.MODE) { case 'server': @@ -101,4 +101,4 @@ async function startApp() { } } -startApp(); +export { startApp }; diff --git a/core/src/shared.ts b/core/src/shared.ts new file mode 100644 index 0000000..6bd2b71 --- /dev/null +++ b/core/src/shared.ts @@ -0,0 +1,34 @@ +import { readdirSync, statSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import logger from './config/logger.js'; +import PluginLoader from './plugins/plugin-loader.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function initializeSharedResources(pluginDirs: string[]) { + const pluginLoader = new PluginLoader(); + + pluginLoader.loadPlugins(pluginDirs); + pluginLoader.initializePlugins(); + + logger.info('Shared resources initialized'); + return pluginLoader; +} + +async function loadPlugins(pluginsPath: string): Promise { + const pluginDirs: string[] = []; + const items = readdirSync(pluginsPath); + + for (const item of items) { + const itemPath = join(pluginsPath, item); + if (statSync(itemPath).isDirectory()) { + pluginDirs.push(itemPath); + } + } + + return pluginDirs; +} + +export { initializeSharedResources }; diff --git a/src/types/express.d.ts b/core/src/types/express.d.ts similarity index 100% rename from src/types/express.d.ts rename to core/src/types/express.d.ts diff --git a/src/types/jwt-payload.d.ts b/core/src/types/jwt-payload.d.ts similarity index 100% rename from src/types/jwt-payload.d.ts rename to core/src/types/jwt-payload.d.ts diff --git a/src/utils/introspection-check.ts b/core/src/utils/introspection-check.ts similarity index 100% rename from src/utils/introspection-check.ts rename to core/src/utils/introspection-check.ts diff --git a/src/utils/should-bypass-auth.ts b/core/src/utils/should-bypass-auth.ts similarity index 100% rename from src/utils/should-bypass-auth.ts rename to core/src/utils/should-bypass-auth.ts diff --git a/src/worker.ts b/core/src/worker.ts similarity index 100% rename from src/worker.ts rename to core/src/worker.ts diff --git a/core/tsconfig.json b/core/tsconfig.json new file mode 100644 index 0000000..6d18b71 --- /dev/null +++ b/core/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", // This should be relative to the core directory + "module": "ESNext", + "target": "ESNext" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/example-app/.env.example b/example-app/.env.example new file mode 100644 index 0000000..ffd2ae6 --- /dev/null +++ b/example-app/.env.example @@ -0,0 +1,10 @@ +# MongoDB connection string +MONGO_URI=mongodb://root:example@localhost:27017/phoenix?authSource=admin +# Server port +PORT=4000 +# Your unique JWT secret +JWT_SECRET=your_jwt_secret +# Log level +LOG_LEVEL=debug + +KAFKAJS_NO_PARTITIONER_WARNING=1 diff --git a/example-app/package.json b/example-app/package.json new file mode 100644 index 0000000..25dc7d1 --- /dev/null +++ b/example-app/package.json @@ -0,0 +1,18 @@ +{ + "name": "myapp", + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "bun run src/index.js" + }, + "dependencies": { + "@phoenix-framework/core": "file:../core", + "graphql": "^16.8.1", + "type-graphql": "^2.0.0-rc.1", + "typedi": "^0.10.0", + "mongoose": "^8.4.1", + "bullmq": "^5.8.2", + "express": "^4.19.2" + } +} diff --git a/example-app/src/index.ts b/example-app/src/index.ts new file mode 100644 index 0000000..257f4fe --- /dev/null +++ b/example-app/src/index.ts @@ -0,0 +1,40 @@ +import dotenv from 'dotenv'; +import { readdirSync, statSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import logger from '@phoenix-framework/core/src/config/logger.js'; +import { startApp } from '@phoenix-framework/core/src/server.js'; +import packageJson from '../package.json' assert { type: 'json' }; + +// Resolve the path to the .env file +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +dotenv.config({ path: join(__dirname, '../.env') }); + +async function loadPlugins(): Promise { + const pluginDirs: string[] = []; + const pluginsPath = join(__dirname, 'plugins'); + const items = readdirSync(pluginsPath); + + for (const item of items) { + const itemPath = join(pluginsPath, item); + if (statSync(itemPath).isDirectory()) { + pluginDirs.push(itemPath); + } + } + + return pluginDirs; +} + +async function runApp() { + try { + const pluginDirs = await loadPlugins(); + await startApp(pluginDirs); // Pass the plugin directories to the core's startApp + logger.info(`Server started, version ${packageJson.version}`); + } catch (error) { + logger.error(error); + process.exit(1); + } +} + +runApp(); diff --git a/src/plugins/auth-plugin/bootstrap.ts b/example-app/src/plugins/auth-plugin/bootstrap.ts similarity index 100% rename from src/plugins/auth-plugin/bootstrap.ts rename to example-app/src/plugins/auth-plugin/bootstrap.ts diff --git a/src/plugins/auth-plugin/index.ts b/example-app/src/plugins/auth-plugin/index.ts similarity index 52% rename from src/plugins/auth-plugin/index.ts rename to example-app/src/plugins/auth-plugin/index.ts index b6c6180..13ff65c 100644 --- a/src/plugins/auth-plugin/index.ts +++ b/example-app/src/plugins/auth-plugin/index.ts @@ -1,8 +1,8 @@ -import { AuthResolver } from './resolvers/auth-resolver'; -import type { Plugin } from '../plugin-interface'; -import FunctionRegistry from '../function-registry'; -import { type GlobalContext } from '../global-context'; -import logger from '../../config/logger.ts'; +import { AuthResolver } from './resolvers/auth-resolver.js'; +import { type Plugin } from '@phoenix-framework/core/src/plugins/plugin-interface.js'; +import FunctionRegistry from '@phoenix-framework/core/src/plugins/function-registry.js'; +import { type GlobalContext } from '@phoenix-framework/core/src/plugins/global-context.js'; +import logger from '@phoenix-framework/core/src/config/logger.js'; const loggerCtx = { context: 'auth-plugin/register' }; @@ -10,7 +10,7 @@ const authPlugin: Plugin = { name: 'auth-plugin', type: 'authorization', resolvers: [AuthResolver], - register: (container: any, context: GlobalContext) => { + register: (container, context: GlobalContext) => { // Register resolvers context.resolvers['Auth'] = authPlugin.resolvers ?? []; diff --git a/src/plugins/auth-plugin/models/user.ts b/example-app/src/plugins/auth-plugin/models/user.ts similarity index 100% rename from src/plugins/auth-plugin/models/user.ts rename to example-app/src/plugins/auth-plugin/models/user.ts diff --git a/src/plugins/auth-plugin/resolvers/auth-resolver.ts b/example-app/src/plugins/auth-plugin/resolvers/auth-resolver.ts similarity index 94% rename from src/plugins/auth-plugin/resolvers/auth-resolver.ts rename to example-app/src/plugins/auth-plugin/resolvers/auth-resolver.ts index f002cba..6298ede 100644 --- a/src/plugins/auth-plugin/resolvers/auth-resolver.ts +++ b/example-app/src/plugins/auth-plugin/resolvers/auth-resolver.ts @@ -5,8 +5,8 @@ import { User } from '../models/user'; import { UserService } from '../services/user-service.ts'; import { Service } from 'typedi'; import jwt from 'jsonwebtoken'; -import { getEnforcer } from '../../../rbac'; -import env from '../../../config/config.ts'; +import { getEnforcer } from '@phoenix-framework/core/src/rbac.js' +import env from '@phoenix-framework/core/src/config/config.js'; @Service() // Register AuthResolver with Typedi @Resolver() diff --git a/src/plugins/auth-plugin/services/user-service.test.ts b/example-app/src/plugins/auth-plugin/services/user-service.test.ts similarity index 100% rename from src/plugins/auth-plugin/services/user-service.test.ts rename to example-app/src/plugins/auth-plugin/services/user-service.test.ts diff --git a/src/plugins/auth-plugin/services/user-service.ts b/example-app/src/plugins/auth-plugin/services/user-service.ts similarity index 90% rename from src/plugins/auth-plugin/services/user-service.ts rename to example-app/src/plugins/auth-plugin/services/user-service.ts index f1f8dcc..dfe5307 100644 --- a/src/plugins/auth-plugin/services/user-service.ts +++ b/example-app/src/plugins/auth-plugin/services/user-service.ts @@ -1,7 +1,7 @@ import { Service } from 'typedi'; import { User, UserModel } from '../models/user'; import bcrypt from 'bcrypt'; -import { getEnforcer } from '../../../rbac.ts'; +import { getEnforcer } from '@phoenix-framework/core/src/rbac.js'; @Service() export class UserService { diff --git a/src/plugins/cart-plugin/index.ts b/example-app/src/plugins/cart-plugin/index.ts similarity index 100% rename from src/plugins/cart-plugin/index.ts rename to example-app/src/plugins/cart-plugin/index.ts diff --git a/src/plugins/cart-plugin/models/cart.ts b/example-app/src/plugins/cart-plugin/models/cart.ts similarity index 100% rename from src/plugins/cart-plugin/models/cart.ts rename to example-app/src/plugins/cart-plugin/models/cart.ts diff --git a/src/plugins/cart-plugin/resolvers/cart-resolver.ts b/example-app/src/plugins/cart-plugin/resolvers/cart-resolver.ts similarity index 100% rename from src/plugins/cart-plugin/resolvers/cart-resolver.ts rename to example-app/src/plugins/cart-plugin/resolvers/cart-resolver.ts diff --git a/src/plugins/cart-plugin/resolvers/inputs/item-input.ts b/example-app/src/plugins/cart-plugin/resolvers/inputs/item-input.ts similarity index 100% rename from src/plugins/cart-plugin/resolvers/inputs/item-input.ts rename to example-app/src/plugins/cart-plugin/resolvers/inputs/item-input.ts diff --git a/src/plugins/cart-plugin/services/index.ts b/example-app/src/plugins/cart-plugin/services/index.ts similarity index 100% rename from src/plugins/cart-plugin/services/index.ts rename to example-app/src/plugins/cart-plugin/services/index.ts diff --git a/src/plugins/discount-plugin/index.ts b/example-app/src/plugins/discount-plugin/index.ts similarity index 100% rename from src/plugins/discount-plugin/index.ts rename to example-app/src/plugins/discount-plugin/index.ts diff --git a/src/plugins/sample-plugin/index.ts b/example-app/src/plugins/sample-plugin/index.ts similarity index 79% rename from src/plugins/sample-plugin/index.ts rename to example-app/src/plugins/sample-plugin/index.ts index 09a5fda..2b26ebd 100644 --- a/src/plugins/sample-plugin/index.ts +++ b/example-app/src/plugins/sample-plugin/index.ts @@ -1,12 +1,12 @@ import { Container } from 'typedi'; import { getModelForClass } from '@typegoose/typegoose'; -import { type GlobalContext } from '../global-context'; +import { type GlobalContext } from '@phoenix-framework/core/src/plugins/global-context.js'; +import logger from '@phoenix-framework/core/src/config/logger.js'; import { Sample } from './models/sample'; import { SampleResolver } from './resolvers/sample-resolver'; import { SampleService } from './services/sample-service'; -import KafkaEventService from '../../event/kafka-event-service'; +import { KafkaEventService } from '@phoenix-framework/core/src/event/kafka-event-service.js'; import { Queue, Job } from 'bullmq'; -import logger from '../../config/logger.ts'; const loggerCtx = { context: 'sample-plugin/index' }; @@ -36,18 +36,18 @@ export default { container.set('sampleQueue', sampleQueue); // Register SampleService and KafkaEventService with typedi - container.set(SampleService, new SampleService(Container.get(KafkaEventService), sampleQueue)); + const eventService = container.get(KafkaEventService); // Ensure KafkaEventService is registered + container.set(SampleService, new SampleService(eventService, sampleQueue)); // Ensure SampleResolver is added to context resolvers context.resolvers['Sample'] = [SampleResolver]; // Register the topic with KafkaEventService - const eventService = Container.get(KafkaEventService); eventService.registerTopic('sampleCreated'); // Set up event handlers using the centralized event service eventService.subscribeToEvent('sampleCreated', (sample) => { - logger.debug('Received sampleCreated event:', sample) + logger.debug('Received sampleCreated event:', sample); logger.info(`Sample created: ${sample}`, loggerCtx); // Additional handling logic here }); diff --git a/src/plugins/sample-plugin/models/sample.ts b/example-app/src/plugins/sample-plugin/models/sample.ts similarity index 100% rename from src/plugins/sample-plugin/models/sample.ts rename to example-app/src/plugins/sample-plugin/models/sample.ts diff --git a/src/plugins/sample-plugin/resolvers/sample-resolver.ts b/example-app/src/plugins/sample-plugin/resolvers/sample-resolver.ts similarity index 90% rename from src/plugins/sample-plugin/resolvers/sample-resolver.ts rename to example-app/src/plugins/sample-plugin/resolvers/sample-resolver.ts index 91a05e0..0a144f2 100644 --- a/src/plugins/sample-plugin/resolvers/sample-resolver.ts +++ b/example-app/src/plugins/sample-plugin/resolvers/sample-resolver.ts @@ -2,7 +2,7 @@ import { Resolver, Query, Mutation, Arg } from 'type-graphql'; import { Inject, Service } from 'typedi'; import { Sample } from '../models/sample'; import { SampleService } from '../services/sample-service'; -import FunctionRegistry from '../../function-registry'; +import FunctionRegistry from '@phoenix-framework/core/src/plugins/function-registry.js'; @Service() @Resolver() diff --git a/src/plugins/sample-plugin/services/sample-service.ts b/example-app/src/plugins/sample-plugin/services/sample-service.ts similarity index 85% rename from src/plugins/sample-plugin/services/sample-service.ts rename to example-app/src/plugins/sample-plugin/services/sample-service.ts index c29f4bd..06decf1 100644 --- a/src/plugins/sample-plugin/services/sample-service.ts +++ b/example-app/src/plugins/sample-plugin/services/sample-service.ts @@ -1,8 +1,8 @@ import { Service } from 'typedi'; import { Sample, SampleModel } from '../models/sample'; -import KafkaEventService from '../../../event/kafka-event-service'; +import { KafkaEventService } from '@phoenix-framework/core/src/event/kafka-event-service.js'; import { Queue } from 'bullmq'; -import logger from '../../../config/logger.ts'; +import logger from '@phoenix-framework/core/src/config/logger.js'; @Service() export class SampleService { diff --git a/src/plugins/sample-plugin/services/sample-worker.ts b/example-app/src/plugins/sample-plugin/services/sample-worker.ts similarity index 100% rename from src/plugins/sample-plugin/services/sample-worker.ts rename to example-app/src/plugins/sample-plugin/services/sample-worker.ts diff --git a/example-app/tsconfig.json b/example-app/tsconfig.json new file mode 100644 index 0000000..dd3fe72 --- /dev/null +++ b/example-app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": "../", // Set rootDir to the monorepo root + "module": "ESNext", + "target": "ESNext" + }, + "include": ["src/**/*", "../core/src/**/*"], // Include both example-app and core source files + "exclude": ["node_modules"] +} diff --git a/package.json b/package.json index c41b75a..579a2db 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,21 @@ { "name": "phoenix-framework", - "module": "src/index.ts", - "type": "module", + "version": "1.0.0", + "private": true, + "workspaces": ["core", "example-app"], "scripts": { - "start": "bun run src/index.ts", - "lint": "eslint 'src/**/*.{ts,tsx}'", - "lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix", - "format": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,scss,md}'", - "dev": "nodemon --exec bun run src/index.ts", - "test:e2e": "bun test e2e-tests" - }, - "peerDependencies": { - "typescript": "^5.4.5" - }, - "dependencies": { - "@typegoose/typegoose": "^12.5.0", - "apollo-server-express": "^3.13.0", - "bcrypt": "^5.1.1", - "bullmq": "^5.8.2", - "casbin": "^5.30.0", - "casbin-mongoose-adapter": "^5.3.1", - "dotenv": "^16.4.5", - "envalid": "^8.0.0", - "express": "^4.19.2", - "graphql": "^16.8.1", - "jsonwebtoken": "^9.0.2", - "kafkajs": "^2.2.4", - "mongoose": "^8.4.1", - "reflect-metadata": "^0.2.2", - "type-graphql": "^2.0.0-rc.1", - "typedi": "^0.10.0", - "winston": "^3.13.0", - "winston-daily-rotate-file": "^5.0.0" + "start": "cd example-app && bun run src/index.ts", + "dev": "cd example-app && nodemon --exec bun run src/index.ts", + "lint": "eslint 'core/src/**/*.{ts,tsx}' 'example-app/src/**/*.{ts,tsx}'", + "lint:fix": "eslint 'core/src/**/*.{ts,tsx}' 'example-app/src/**/*.{ts,tsx}' --fix", + "format": "prettier --write 'core/src/**/*.{ts,tsx,js,jsx,json,css,scss,md}' 'example-app/src/**/*.{ts,tsx,js,jsx,json,css,scss,md}'", + "test:e2e": "cd example-app && bun test e2e-tests" }, "devDependencies": { - "@types/bcrypt": "^5.0.2", - "@types/bun": "latest", - "@types/chai": "^4.3.16", - "@types/express": "^4.17.21", - "@types/jsonwebtoken": "^9.0.6", - "@types/node": "^20.14.2", - "@types/supertest": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "chai": "^5.1.1", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "mongodb-memory-server": "^9.3.0", - "nodemon": "^3.1.3", "prettier": "^3.3.2", - "supertest": "^7.0.0", - "ts-mockito": "^2.6.1", - "ts-node": "^10.9.2" - }, - "overrides": { - "@types/express": "^4.17.21" - }, - "resolutions": { - "graphql": "^16.8.1" + "typescript": "^5.4.5" } } diff --git a/src/shared.ts b/src/shared.ts deleted file mode 100644 index a8b2a22..0000000 --- a/src/shared.ts +++ /dev/null @@ -1,25 +0,0 @@ -// src/shared.ts -import { connectToDatabase } from './config/database'; -import { initEnforcer } from './rbac'; -import { bootstrap } from './plugins/auth-plugin/bootstrap'; -import PluginLoader from './plugins/plugin-loader'; -import logger from './config/logger'; - -const loggerCtx = { context: 'shared' }; - -export async function initializeSharedResources() { - await connectToDatabase(); - await initEnforcer(); // Initialize Casbin - await bootstrap(); // Bootstrap the application with a superuser - - const pluginLoader = new PluginLoader(); - pluginLoader.loadPlugins(); - - // Register models before initializing plugins - pluginLoader.registerModels(); - - // Initialize plugins (extend models and resolvers) - pluginLoader.initializePlugins(); - - return pluginLoader; -} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..0eb671a --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "baseUrl": ".", + "paths": { + "@phoenix-framework/core/*": ["core/src/*"] + } + }, + "include": ["core/src/**/*.ts", "example-app/src/**/*.ts"] +} From 84760531eaddf42f3aac8c343ce38829e5dcef39 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Sun, 14 Jul 2024 17:00:38 +0800 Subject: [PATCH 2/3] chore: server not starting but worker starting --- core/src/plugins/global-context.ts | 1 + core/src/plugins/plugin-loader.ts | 9 ++++-- core/src/server.ts | 29 +++++-------------- core/src/shared.ts | 11 +++---- core/src/worker.ts | 17 ++++++----- example-app/src/index.ts | 2 +- example-app/src/plugins/cart-plugin/index.ts | 12 ++++++-- .../cart-plugin/resolvers/cart-resolver.ts | 6 +--- .../resolvers/inputs/item-input.ts | 16 ++++------ 9 files changed, 48 insertions(+), 55 deletions(-) diff --git a/core/src/plugins/global-context.ts b/core/src/plugins/global-context.ts index 12feaa1..d4e126e 100644 --- a/core/src/plugins/global-context.ts +++ b/core/src/plugins/global-context.ts @@ -8,6 +8,7 @@ export type ResolverMap = { export interface GlobalContext { models: { [key: string]: { schema: Schema; model: any } }; resolvers: { [key: string]: Function[] }; + services: { [key: string]: any }; extendModel: (name: string, extension: (schema: Schema) => void) => void; extendResolvers: (name: string, extension: Function[]) => void; wrapResolver: (name: string, resolver: string, wrapper: Function) => void; diff --git a/core/src/plugins/plugin-loader.ts b/core/src/plugins/plugin-loader.ts index 6053444..776b41e 100644 --- a/core/src/plugins/plugin-loader.ts +++ b/core/src/plugins/plugin-loader.ts @@ -9,7 +9,7 @@ import mongoose, { type Schema } from 'mongoose'; import { type GlobalContext } from './global-context.js'; import { type Plugin } from './plugin-interface.js'; import { Queue, Worker, QueueEvents } from 'bullmq'; -import env from '../config/config.js'; + const loggerCtx = { context: 'plugin-loader' }; const __filename = fileURLToPath(import.meta.url); @@ -24,6 +24,7 @@ class PluginLoader { return { models: {}, resolvers: {}, + services: {}, extendModel: this.extendModel.bind(this), extendResolvers: this.extendResolvers.bind(this), wrapResolver: this.wrapResolver.bind(this), @@ -62,8 +63,10 @@ class PluginLoader { resolverArray[originalResolverIndex].prototype[resolverName] = wrapper(originalResolver); } - loadPlugins(pluginDirs: string[]): void { - pluginDirs.forEach(this.loadPlugin.bind(this)); + async loadPlugins(pluginDirs: string[]): Promise { + for (const pluginDir of pluginDirs) { + await this.loadPlugin(pluginDir); + } } private async loadPlugin(pluginDir: string): Promise { diff --git a/core/src/server.ts b/core/src/server.ts index 25bb8a8..c5c2130 100644 --- a/core/src/server.ts +++ b/core/src/server.ts @@ -1,6 +1,5 @@ -import 'reflect-metadata'; -import express, { type Application, type Request, type Response, type NextFunction } from 'express'; import { ApolloServer } from 'apollo-server-express'; +import express, { type Application, type Request, type Response, type NextFunction } from 'express'; import env from './config/config.js'; import logger from './config/logger.js'; import { authenticate } from './middleware/auth.js'; @@ -10,11 +9,12 @@ import sanitizeLog from './sanitize-log.js'; import { initializeSharedResources } from './shared.js'; import { startWorker } from './worker.js'; import { getEnforcer } from './rbac.js'; -import type PluginLoader from './plugins/plugin-loader.js'; +import PluginLoader from './plugins/plugin-loader.js'; const loggerCtx = { context: 'server' }; async function startServer(pluginLoader: PluginLoader) { + console.log('startServer'); try { const schema = await pluginLoader.createSchema(); @@ -72,7 +72,7 @@ async function startServer(pluginLoader: PluginLoader) { }); server.applyMiddleware({ app }); - + console.log('applyMiddleware'); const port = env.PORT; app.listen(port, () => { logger.info(`Server is running at http://localhost:${port}${server.graphqlPath}`, { context: 'server' }); @@ -82,23 +82,10 @@ async function startServer(pluginLoader: PluginLoader) { } } -async function startApp(pluginDirs: string[]) { +export async function startApp(pluginDirs: string[]) { const pluginLoader = await initializeSharedResources(pluginDirs); - switch (env.MODE) { - case 'server': - await startServer(pluginLoader); - break; - case 'worker': - await startWorker(pluginLoader); - break; - case 'dev': - await startServer(pluginLoader); - await startWorker(pluginLoader, true); // Pass a flag to indicate dev mode - break; - default: - logger.error('Unknown mode specified. Please set MODE to "server", "worker", or "dev".', loggerCtx); - } + // Start the server + await startServer(pluginLoader); + await startWorker(pluginDirs, env.MODE === 'dev'); } - -export { startApp }; diff --git a/core/src/shared.ts b/core/src/shared.ts index 6bd2b71..782ec37 100644 --- a/core/src/shared.ts +++ b/core/src/shared.ts @@ -7,13 +7,14 @@ import PluginLoader from './plugins/plugin-loader.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -async function initializeSharedResources(pluginDirs: string[]) { - const pluginLoader = new PluginLoader(); +async function initializeSharedResources(pluginDirs: string[]): Promise { + if (!pluginDirs) { + throw new Error('pluginDirs must be provided'); + } - pluginLoader.loadPlugins(pluginDirs); + const pluginLoader = new PluginLoader(); + await pluginLoader.loadPlugins(pluginDirs); pluginLoader.initializePlugins(); - - logger.info('Shared resources initialized'); return pluginLoader; } diff --git a/core/src/worker.ts b/core/src/worker.ts index 263be9e..5174cf4 100644 --- a/core/src/worker.ts +++ b/core/src/worker.ts @@ -1,14 +1,17 @@ - -import logger from './config/logger'; -import { initializeSharedResources } from './shared'; -import type PluginLoader from './plugins/plugin-loader'; +import logger from './config/logger.js'; +import { initializeSharedResources } from './shared.js'; +import PluginLoader from './plugins/plugin-loader.js'; const loggerCtx = { context: 'worker' }; -export async function startWorker(pluginLoader: PluginLoader, isDevMode = false) { +export async function startWorker(pluginDirs: string[], isDevMode = false) { + let pluginLoader: PluginLoader; try { - if (!isDevMode) { - pluginLoader = await initializeSharedResources(); + pluginLoader = await initializeSharedResources(pluginDirs); + + if (isDevMode) { + // Additional development mode-specific logic + logger.info('Worker running in development mode', loggerCtx); } pluginLoader.initializeQueues(); diff --git a/example-app/src/index.ts b/example-app/src/index.ts index 257f4fe..b0cb594 100644 --- a/example-app/src/index.ts +++ b/example-app/src/index.ts @@ -30,7 +30,7 @@ async function runApp() { try { const pluginDirs = await loadPlugins(); await startApp(pluginDirs); // Pass the plugin directories to the core's startApp - logger.info(`Server started, version ${packageJson.version}`); + logger.info(`Server started, version ${packageJson.version}`, 'exampleApp-index'); } catch (error) { logger.error(error); process.exit(1); diff --git a/example-app/src/plugins/cart-plugin/index.ts b/example-app/src/plugins/cart-plugin/index.ts index cf271e8..8985bcd 100644 --- a/example-app/src/plugins/cart-plugin/index.ts +++ b/example-app/src/plugins/cart-plugin/index.ts @@ -1,8 +1,9 @@ import { Container } from 'typedi'; import { getModelForClass } from '@typegoose/typegoose'; -import { type GlobalContext } from '../global-context'; +import { type GlobalContext } from '@phoenix-framework/core/src/plugins/global-context'; import { Cart } from './models/cart'; import { CartResolver } from './resolvers/cart-resolver'; +import { CartService } from './services'; export default { name: 'cart-plugin', @@ -12,7 +13,14 @@ export default { const CartModel = getModelForClass(Cart); context.models['Cart'] = { schema: CartModel.schema, model: CartModel }; container.set('CartModel', CartModel); - container.set(CartResolver, new CartResolver()); // Register CartResolver explicitly + + // Register CartService explicitly + container.set(CartService, new CartService()); + + // Correctly instantiate CartResolver with CartService + const cartService = container.get(CartService); + container.set(CartResolver, new CartResolver(cartService)); + context.extendResolvers('Cart', [CartResolver]); const resolverMethods = Object.getOwnPropertyNames(CartResolver.prototype).filter( (method) => method !== 'constructor', diff --git a/example-app/src/plugins/cart-plugin/resolvers/cart-resolver.ts b/example-app/src/plugins/cart-plugin/resolvers/cart-resolver.ts index 00c30f8..1b832ff 100644 --- a/example-app/src/plugins/cart-plugin/resolvers/cart-resolver.ts +++ b/example-app/src/plugins/cart-plugin/resolvers/cart-resolver.ts @@ -8,11 +8,7 @@ import { Item } from '../models/cart'; @Service() @Resolver(() => Cart) export class CartResolver { - private cartService: CartService; - - constructor() { - this.cartService = new CartService(); - } + constructor(private readonly cartService: CartService) {} @Query(() => [Cart]) async getCarts(): Promise { diff --git a/example-app/src/plugins/cart-plugin/resolvers/inputs/item-input.ts b/example-app/src/plugins/cart-plugin/resolvers/inputs/item-input.ts index 79dbcd7..52deee7 100644 --- a/example-app/src/plugins/cart-plugin/resolvers/inputs/item-input.ts +++ b/example-app/src/plugins/cart-plugin/resolvers/inputs/item-input.ts @@ -1,25 +1,19 @@ import { InputType, Field, Int, Float } from 'type-graphql'; -import { prop } from '@typegoose/typegoose'; @InputType() export class ItemInput { @Field() - @prop({ required: true }) - public name!: string; + name!: string; @Field() - @prop({ required: true }) - public description!: string; + description!: string; @Field() - @prop({ required: true }) - public productId!: string; + productId!: string; @Field(() => Int) - @prop({ required: true }) - public quantity!: number; + quantity!: number; @Field(() => Float) - @prop({ required: true }) - public price!: number; + price!: number; } From 02a37da02a85c350c4b02325431a8989f5f28c02 Mon Sep 17 00:00:00 2001 From: Brent Hoover Date: Sun, 14 Jul 2024 17:26:37 +0800 Subject: [PATCH 3/3] fix: app server starts but problems with cart plugin --- core/src/plugins/plugin-loader.ts | 5 ++-- core/src/server.ts | 40 ++++++++++++++++++------------- core/src/shared.ts | 11 +++++---- example-app/src/index.ts | 4 ++-- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/core/src/plugins/plugin-loader.ts b/core/src/plugins/plugin-loader.ts index 776b41e..1a7cb38 100644 --- a/core/src/plugins/plugin-loader.ts +++ b/core/src/plugins/plugin-loader.ts @@ -5,12 +5,11 @@ import { statSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import logger from '../config/logger.js'; -import mongoose, { type Schema } from 'mongoose'; +import mongoose, { Schema } from 'mongoose'; import { type GlobalContext } from './global-context.js'; import { type Plugin } from './plugin-interface.js'; import { Queue, Worker, QueueEvents } from 'bullmq'; - const loggerCtx = { context: 'plugin-loader' }; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -110,7 +109,7 @@ class PluginLoader { return await buildSchema({ resolvers: allResolvers as unknown as NonEmptyArray, container: Container, - emitSchemaFile: join(__dirname, '../../schema.graphql'), + emitSchemaFile: true, }); } catch (error) { logger.error(`Error building schema: ${error}`, loggerCtx); diff --git a/core/src/server.ts b/core/src/server.ts index c5c2130..92e494d 100644 --- a/core/src/server.ts +++ b/core/src/server.ts @@ -1,5 +1,6 @@ -import { ApolloServer } from 'apollo-server-express'; +import 'reflect-metadata'; import express, { type Application, type Request, type Response, type NextFunction } from 'express'; +import { ApolloServer } from 'apollo-server-express'; import env from './config/config.js'; import logger from './config/logger.js'; import { authenticate } from './middleware/auth.js'; @@ -14,17 +15,16 @@ import PluginLoader from './plugins/plugin-loader.js'; const loggerCtx = { context: 'server' }; async function startServer(pluginLoader: PluginLoader) { - console.log('startServer'); try { const schema = await pluginLoader.createSchema(); const server = new ApolloServer({ schema, - introspection: true, // Ensure introspection is enabled + introspection: true, context: async ({ req }) => ({ - user: req.user, // User object from middleware - enforcer: await getEnforcer(), // Casbin enforcer instance - pluginsContext: pluginLoader.context, // Add the global context here + user: req.user, + enforcer: await getEnforcer(), + pluginsContext: pluginLoader.context, }), }); @@ -34,7 +34,6 @@ async function startServer(pluginLoader: PluginLoader) { app.use(express.json()); - // Middleware to conditionally authenticate user and set user context app.use('/graphql', (req: Request, res: Response, next: NextFunction) => { const reqInfo = { url: req.url, @@ -47,23 +46,21 @@ async function startServer(pluginLoader: PluginLoader) { if (req.body && req.body.query) { if (isIntrospectionQuery(req.body.query)) { logger.verbose('Bypassing authentication for introspection query', loggerCtx); - return next(); // Bypass authentication for introspection queries + return next(); } if (shouldBypassAuth(req.body.query)) { - logger.verbose(`Bypassing authentication due to excluded operation`, loggerCtx); - return next(); // Bypass authentication for this request + logger.verbose('Bypassing authentication due to excluded operation', loggerCtx); + return next(); } try { - // If no operation bypasses authentication, apply authentication middleware authenticate(req, res, next); } catch (error) { logger.error('Error parsing GraphQL query:', { error, query: req.body.query }); authenticate(req, res, next); } } else { - // If there is no query in the request body, continue with authentication authenticate(req, res, next); } const sanitizedReqInfo = sanitizeLog(reqInfo); @@ -72,10 +69,13 @@ async function startServer(pluginLoader: PluginLoader) { }); server.applyMiddleware({ app }); - console.log('applyMiddleware'); - const port = env.PORT; - app.listen(port, () => { - logger.info(`Server is running at http://localhost:${port}${server.graphqlPath}`, { context: 'server' }); + + const port = env.PORT || 3000; + await new Promise((resolve) => { + app.listen(port, () => { + logger.info(`Server is running at http://localhost:${port}${server.graphqlPath}`, { context: 'server' }); + resolve(); + }); }); } catch (error) { logger.error('Failed to start server:', error, loggerCtx); @@ -83,9 +83,15 @@ async function startServer(pluginLoader: PluginLoader) { } export async function startApp(pluginDirs: string[]) { + if (!pluginDirs) { + throw new Error('pluginDirs must be provided'); + } + const pluginLoader = await initializeSharedResources(pluginDirs); // Start the server await startServer(pluginLoader); - await startWorker(pluginDirs, env.MODE === 'dev'); + // await startWorker(pluginDirs, env.MODE === 'dev'); + + logger.info(`Server started, version 1.0.0`); } diff --git a/core/src/shared.ts b/core/src/shared.ts index 782ec37..f80ab0b 100644 --- a/core/src/shared.ts +++ b/core/src/shared.ts @@ -6,15 +6,18 @@ import PluginLoader from './plugins/plugin-loader.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +const loggerCtx = { context: 'shared' }; async function initializeSharedResources(pluginDirs: string[]): Promise { - if (!pluginDirs) { - throw new Error('pluginDirs must be provided'); - } - const pluginLoader = new PluginLoader(); + + logger.info('Initializing shared resources', loggerCtx); + await pluginLoader.loadPlugins(pluginDirs); pluginLoader.initializePlugins(); + + logger.info('Shared resources initialized', loggerCtx); + return pluginLoader; } diff --git a/example-app/src/index.ts b/example-app/src/index.ts index b0cb594..30c0ee6 100644 --- a/example-app/src/index.ts +++ b/example-app/src/index.ts @@ -29,8 +29,8 @@ async function loadPlugins(): Promise { async function runApp() { try { const pluginDirs = await loadPlugins(); - await startApp(pluginDirs); // Pass the plugin directories to the core's startApp - logger.info(`Server started, version ${packageJson.version}`, 'exampleApp-index'); + await startApp(pluginDirs); + logger.info(`Application initialized, version ${packageJson.version}`); } catch (error) { logger.error(error); process.exit(1);