Skip to content

Commit

Permalink
Okay, this is the doozy. We are now adding a createInfrastructure fun…
Browse files Browse the repository at this point in the history
…ction that does a lot of things for us.

1. Creates all of the default routers, integration, auth, user, entity
2. Creates the serverless.yml definition programmatically based on configured integrations
3. Adds a new router, queue, and queue worker for every integration by default
4. Adds a websocket handler just because we can? Largely for demos but useful in a lot of other contexts too.
5. Sets the stage for iterating

TODO:
[] Need to figure out if this works with a locally defined serverless.yml / whether we should support that
[] Webpack was having a lot of issues due to it being referenced from a different scope/directory... really unclear how to resolve this, but probably critical due to build size
[] Add some more override options to the serverless.yml definition based on the overall Frigg App definition provided
[] Get the Frigg App definition handled at root instead of in the backend, so we can also run the frontend at the same time (if it exists)
[] Tests, tests, and more tests needed
[] This needs to be tested for build and deploy. Unclear if it does yet, though running sls offline usually indicates we're alright
[] Check defaults and have different defaults for when someone wants KMS Key vs. env, when they want to use Secrets Manager, and probably a few others that are common/critical (like domain management and VPC stuff).
[] More TODOs but those will become apparent when working through the tests and using the `frigg start` command
[] add options to frigg-start?
[] support for TS?
  • Loading branch information
seanspeaks committed Oct 17, 2024
1 parent 7d0a8ee commit 9bc9068
Show file tree
Hide file tree
Showing 14 changed files with 707 additions and 4 deletions.
6 changes: 4 additions & 2 deletions packages/devtools/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const test = require('./test');
const { createFriggInfrastructure } = require('./infrastructure');

module.exports = {
...test
}
createFriggInfrastructure,
...test,
};
57 changes: 57 additions & 0 deletions packages/devtools/infrastructure/app-handler-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { createHandler, flushDebugLog } = require('@friggframework/core');
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const Boom = require('@hapi/boom');
const loadUserManager = require('./routers/middleware/loadUser');
const serverlessHttp = require('serverless-http');

const createApp = (applyMiddleware) => {
const app = express();

app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(
cors({
origin: '*',
credentials: true,
})
);

app.use(loadUserManager);

if (applyMiddleware) applyMiddleware(app);

// Handle sending error response and logging server errors to console
app.use((err, req, res, next) => {
const boomError = err.isBoom ? err : Boom.boomify(err);
const {
output: { statusCode = 500 },
} = boomError;

if (statusCode >= 500) {
flushDebugLog(boomError);
res.status(statusCode).json({ error: 'Internal Server Error' });
} else {
res.status(statusCode).json({ error: err.message });
}
});

return app;
};

function createAppHandler(eventName, router, shouldUseDatabase = true) {
const app = createApp((app) => {
app.use(router);
});
return createHandler({
eventName,
method: serverlessHttp(app),
shouldUseDatabase,
});
}

module.exports = {
createApp,
createAppHandler,
};
90 changes: 90 additions & 0 deletions packages/devtools/infrastructure/backend-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const { createFriggBackend, Worker } = require('@friggframework/core');
const {
findNearestBackendPackageJson,
} = require('../frigg-cli/utils/backend-path');
const path = require('path');
const fs = require('fs-extra');

const backendPath = findNearestBackendPackageJson();
if (!backendPath) {
throw new Error('Could not find backend package.json');
}

const backendDir = path.dirname(backendPath);
const backendFilePath = path.join(backendDir, 'index.js');
if (!fs.existsSync(backendFilePath)) {
throw new Error('Could not find index.js');
}

const backendJsFile = require(backendFilePath);
const { Router } = require('express');
const appDefinition = backendJsFile.Definition;

const backend = createFriggBackend(appDefinition);
const loadRouterFromObject = (IntegrationClass, routerObject) => {
const router = Router();
const { path, method, event } = routerObject;
console.log(
`Registering ${method} ${path} for ${IntegrationClass.Definition.name}`
);
router[method.toLowerCase()](path, async (req, res, next) => {
try {
const integration = new IntegrationClass({});
await integration.loadModules();
await integration.registerEventHandlers();
const result = await integration.send(event, req.body);
res.json(result);
} catch (error) {
next(error);
}
});

return router;
};
const createQueueWorker = (integrationClass) => {
class QueueWorker extends Worker {
constructor(params) {
super(params);
}
async _run(params, context) {
try {
let instance;
if (!params.integrationId) {
instance = new integrationClass({});
await instance.loadModules();
// await instance.loadUserActions();
await instance.registerEventHandlers();
console.log(
`${params.event} for ${integrationClass.Definition.name} integration with no integrationId`
);
} else {
instance =
await integrationClass.getInstanceFromIntegrationId({
integrationId: params.integrationId,
});
console.log(
`${params.event} for ${instance.integration.config.type} of integrationId: ${params.integrationId}`
);
}
const res = await instance.send(params.event, {
data: params.data,
context,
});
return res;
} catch (error) {
console.error(
`Error in ${params.event} for ${integrationClass.Definition.name}:`,
error
);
throw error;
}
}
}
return QueueWorker;
};

module.exports = {
...backend,
loadRouterFromObject,
createQueueWorker,
};
38 changes: 38 additions & 0 deletions packages/devtools/infrastructure/create-frigg-infrastructure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const path = require('path');
const fs = require('fs-extra');
const { composeServerlessDefinition } = require('./serverless-template');

const {
findNearestBackendPackageJson,
} = require('../frigg-cli/utils/backend-path');

function createFriggInfrastructure() {
const backendPath = findNearestBackendPackageJson();
if (!backendPath) {
throw new Error('Could not find backend package.json');
}

const backendDir = path.dirname(backendPath);
const backendFilePath = path.join(backendDir, 'index.js');
if (!fs.existsSync(backendFilePath)) {
throw new Error('Could not find index.js');
}

const backend = require(backendFilePath);
const appDefinition = backend.Definition;

// const serverlessTemplate = require(path.resolve(
// __dirname,
// './serverless-template.js'
// ));
const definition = composeServerlessDefinition(
appDefinition,
backend.IntegrationFactory
);

return {
...definition,
};
}

module.exports = { createFriggInfrastructure };
4 changes: 4 additions & 0 deletions packages/devtools/infrastructure/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { createFriggInfrastructure } = require('./create-frigg-infrastructure');
module.exports = {
createFriggInfrastructure,
};
26 changes: 26 additions & 0 deletions packages/devtools/infrastructure/routers/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { createIntegrationRouter } = require('@friggframework/core');
const { createAppHandler } = require('./../app-handler-helpers');
const { requireLoggedInUser } = require('./middleware/requireLoggedInUser');
const {
moduleFactory,
integrationFactory,
IntegrationHelper,
} = require('./../backend-utils');

const router = createIntegrationRouter({
factory: { moduleFactory, integrationFactory, IntegrationHelper },
requireLoggedInUser,
getUserId: (req) => req.user.getUserId(),
});

router.route('/redirect/:appId').get((req, res) => {
res.redirect(
`${process.env.FRONTEND_URI}/redirect/${
req.params.appId
}?${new URLSearchParams(req.query)}`
);
});

const handler = createAppHandler('HTTP Event: Auth', router);

module.exports = { handler, router };
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { createIntegrationRouter } = require('@friggframework/core');
const { createAppHandler } = require('./../app-handler-helpers');
const { requireLoggedInUser } = require('./middleware/requireLoggedInUser');
const {
moduleFactory,
integrationFactory,
IntegrationHelper,
loadRouterFromObject,
} = require('./../backend-utils');
const { Router } = require('express');

const handlers = {};
integrationFactory.integrationClasses.forEach((IntegrationClass) => {
const router = Router();
const basePath = `/api/${IntegrationClass.Definition.name}-integration`;
IntegrationClass.Definition.routes.forEach((routeDef) => {
if (typeof routeDef === 'function') {
router.use(basePath, routeDef(IntegrationClass));
} else if (typeof routeDef === 'object') {
router.use(
basePath,
loadRouterFromObject(IntegrationClass, routeDef)
);
} else if (routeDef instanceof express.Router) {
router.use(basePath, routeDef);
}
});

handlers[`${IntegrationClass.Definition.name}`] = {
handler: createAppHandler(
`HTTP Event: ${IntegrationClass.Definition.name}`,
router
),
};
});

module.exports = { handlers };
15 changes: 15 additions & 0 deletions packages/devtools/infrastructure/routers/middleware/loadUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const catchAsyncError = require('express-async-handler');
const { User } = require('../../backend-utils');

module.exports = catchAsyncError(async (req, res, next) => {
const authorizationHeader = req.headers.authorization;

if (authorizationHeader) {
// Removes "Bearer " and trims
const token = authorizationHeader.split(' ')[1].trim();
// Load user for later middleware/routes to use
req.user = await User.newUser({ token });
}

return next();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const Boom = require('@hapi/boom');

// CheckLoggedIn Middleware
const requireLoggedInUser = (req, res, next) => {
if (!req.user || !req.user.isLoggedIn()) {
throw Boom.unauthorized('Invalid Token');
}

next();
};

module.exports = { requireLoggedInUser };
41 changes: 41 additions & 0 deletions packages/devtools/infrastructure/routers/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const express = require('express');
const { createAppHandler } = require('../app-handler-helpers');
const { checkRequiredParams } = require('@friggframework/core');
const { User } = require('../backend-utils');
const catchAsyncError = require('express-async-handler');

const router = express();

// define the login endpoint
router.route('/user/login').post(
catchAsyncError(async (req, res) => {
const { username, password } = checkRequiredParams(req.body, [
'username',
'password',
]);
const user = await User.loginUser({ username, password });
const token = await user.createUserToken(120);
res.status(201);
res.json({ token });
})
);

router.route('/user/create').post(
catchAsyncError(async (req, res) => {
const { username, password } = checkRequiredParams(req.body, [
'username',
'password',
]);
const user = await User.createIndividualUser({
username,
password,
});
const token = await user.createUserToken(120);
res.status(201);
res.json({ token });
})
);

const handler = createAppHandler('HTTP Event: User', router);

module.exports = { handler, router };
55 changes: 55 additions & 0 deletions packages/devtools/infrastructure/routers/websocket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const { createHandler } = require('@friggframework/core');
const { WebsocketConnection } = require('@friggframework/core');

const handleWebSocketConnection = async (event, context) => {
// Handle different WebSocket events
switch (event.requestContext.eventType) {
case 'CONNECT':
// Handle new connection
try {
const connectionId = event.requestContext.connectionId;
await WebsocketConnection.create({ connectionId });
console.log(`Stored new connection: ${connectionId}`);
return { statusCode: 200, body: 'Connected.' };
} catch (error) {
console.error('Error storing connection:', error);
return { statusCode: 500, body: 'Error connecting.' };
}

case 'DISCONNECT':
// Handle disconnection
try {
const connectionId = event.requestContext.connectionId;
await WebsocketConnection.deleteOne({ connectionId });
console.log(`Removed connection: ${connectionId}`);
return { statusCode: 200, body: 'Disconnected.' };
} catch (error) {
console.error('Error removing connection:', error);
return { statusCode: 500, body: 'Error disconnecting.' };
}

case 'MESSAGE':
// Handle incoming message
const message = JSON.parse(event.body);
console.log('Received message:', message);

// Process the message and send a response
const responseMessage = { message: 'Message received' };
return {
statusCode: 200,
body: JSON.stringify(responseMessage),
};

default:
return { statusCode: 400, body: 'Unhandled event type.' };
}
};

const handler = createHandler({
eventName: 'WebSocket Event',
method: handleWebSocketConnection,
shouldUseDatabase: true, // Set to true as we're using the database
isUserFacingResponse: true, // This is a server-to-server response
});

module.exports = { handler };
Loading

0 comments on commit 9bc9068

Please sign in to comment.