diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 000000000..11ddd8dbe --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,3 @@ +node_modules +# Keep environment variables out of version control +.env diff --git a/api/SAMPLE.env b/api/SAMPLE.env new file mode 100644 index 000000000..32b4c775a --- /dev/null +++ b/api/SAMPLE.env @@ -0,0 +1,7 @@ +# Environment variables declared in this file are automatically made available to Prisma. +# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema + +# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. +# See the documentation for all the connection string options: https://pris.ly/d/connection-strings + +DATABASE_URL="mongodb+srv://test:test@cluster0.ns1yp.mongodb.net/myFirstDatabase" \ No newline at end of file diff --git a/api/package.json b/api/package.json new file mode 100644 index 000000000..78f147060 --- /dev/null +++ b/api/package.json @@ -0,0 +1,30 @@ +{ + "name": "demo-api", + "version": "0.1.0", + "description": "", + "main": "src/index.ts", + "private": true, + "scripts": { + "start": "nodemon src/index.ts", + "build": "tsc", + "serve": "node dist/index.js" + }, + "author": "Matt Glissmann", + "license": "ISC", + "dependencies": { + "@prisma/client": "5.5.2" + }, + "devDependencies": { + "@types/cors": "^2.8.15", + "@types/express": "^4.17.20", + "@types/express-validator": "^3.0.0", + "@types/node": "^20.8.9", + "cors": "^2.8.5", + "express": "^4.18.2", + "express-validator": "^7.0.1", + "nodemon": "^3.0.2", + "prisma": "^5.5.2", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + } +} diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma new file mode 100644 index 000000000..e4c6df517 --- /dev/null +++ b/api/prisma/schema.prisma @@ -0,0 +1,86 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(auto()) @map("_id") @db.ObjectId + email String @unique + name String? + posts Post[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Post { + id String @id @default(auto()) @map("_id") @db.ObjectId + title String + content String? + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId String @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Component { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + status String? + description String? + keywords String[] + resources Resource[] + anatomy Anatomy[] + states ComponentState[] + configurations Configuration[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Anatomy is the physical structure of a component and describes the parts from which it is made +model Anatomy { + id String @id @default(auto()) @map("_id") @db.ObjectId + component Component @relation(fields: [componentId], references: [id]) + componentId String @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// ComponentState represent various interactive states of a component +model ComponentState { + id String @id @default(auto()) @map("_id") @db.ObjectId + component Component @relation(fields: [componentId], references: [id]) + componentId String @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Configuration is a variant or flavor of a component which can be modified via its properties +model Configuration { + id String @id @default(auto()) @map("_id") @db.ObjectId + component Component @relation(fields: [componentId], references: [id]) + componentId String @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Resource is a file or link that is associated with a component +model Resource { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + // types may include: design, code/implementation, documentation, demo, example use cases, + // research, design criteria, design rationale, etc. + type String + url String + component Component? @relation(fields: [componentId], references: [id]) + componentId String @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/api/src/components/actions.ts b/api/src/components/actions.ts new file mode 100644 index 000000000..25cfe33f2 --- /dev/null +++ b/api/src/components/actions.ts @@ -0,0 +1,139 @@ +import { PrismaClient } from '@prisma/client'; +import { Request, Response } from 'express'; + +const prisma = new PrismaClient(); + +const createComponent = async (req: Request, res: Response) => { + if (!req.body) { + return res.status(400).json({ error: 'Invalid request. Request body is required.' }); + } + + const { name, description, keywords, status } = req.body; + + const component = await prisma.component.create({ + data: { + name, + description, + keywords, + status, + }, + }); + + return res.json(component); +} + +const updateComponent = async (req: Request, res: Response) => { + const { id, name, description, keywords, status } = req.body; + + const component = await prisma.component.findUnique({ + where: { + id: id, + }, + }); + + if (!component) { + res.status(404).send('Component not found'); + } else { + const updatedComponent = await prisma.component.update({ + where: { + id: id, + }, + data: { + name: name || component.name, + description: description || component.description, + keywords: keywords || component.keywords, + status: status || component.status, + }, + }); + + return res.json(updatedComponent); + } +} + +const createAndAddResource = async (req: Request, res: Response) => { + const { name, type, url, componentId } = req.body; + + const resource = await prisma.resource.create({ + data: { + name, + type, + url, + component: { + connect: { + id: componentId, + }, + }, + }, + }); + + return res.json(resource); +} + +const addResource = async (req: Request, res: Response) => { + const { id, resourceId } = req.body; + + const component = await prisma.component.findUnique({ + where: { + id: id, + }, + }); + + if (!component) { + res.status(404).send('Component not found'); + } else { + const updatedComponent = await prisma.component.update({ + where: { + id: id, + }, + data: { + resources: { + connect: { + id: resourceId, + }, + }, + }, + }); + + return res.json(updatedComponent); + } +} + +const removeResource = async (req: Request, res: Response) => { + const { id, resourceId } = req.body; + + const component = await prisma.component.findUnique({ + where: { + id: id, + }, + }); + + if (!component) { + res.status(404).send('Component not found'); + } else { + const updatedComponent = await prisma.component.update({ + where: { + id: id, + }, + data: { + resources: { + disconnect: { + id: resourceId, + }, + }, + }, + include: { + resources: true, + }, + }); + + return res.json(updatedComponent); + } +} + +export { + createComponent, + updateComponent, + addResource, + createAndAddResource, + removeResource +} \ No newline at end of file diff --git a/api/src/components/routes.ts b/api/src/components/routes.ts new file mode 100644 index 000000000..acde47a8d --- /dev/null +++ b/api/src/components/routes.ts @@ -0,0 +1,165 @@ +import { Router, Request, Response } from 'express'; +import { validationResult } from 'express-validator'; +import { PrismaClient } from '@prisma/client'; +import { + createComponent, + updateComponent, + addResource, + createAndAddResource, + removeResource +} from './actions'; +import { createRules, updateRules } from './validation-rules'; + +const prisma = new PrismaClient(); +const router = Router(); + +// Components CRUD +router.post('/', createRules, async (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + createComponent(req, res).then(async (component) => { + res.status(201).json(component); + await prisma.$disconnect(); + }).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.get('/', async (req: Request, res: Response) => { + const components = await prisma.component.findMany({ + orderBy: { + name: 'asc', + }, + }); + res.json(components); + await prisma.$disconnect(); +}); + +router.get('/:id', async (req: Request, res: Response) => { + const id = req.params.id; + const component = await prisma.component.findUnique({ + where: { + id: id, + }, + }); + + if (!component) { + res.status(404).send('Component not found'); + } else { + res.json(component); + } + + await prisma.$disconnect(); +}); + +router.put('/:id', updateRules, async (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + updateComponent(req, res).then(async (component) => { + res.json(component); + await prisma.$disconnect(); + }).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.delete('/:id', async (req: Request, res: Response) => { + const id = req.params.id; + const component = await prisma.component.delete({ + where: { + id: id, + }, + }); + + res.json(component); + await prisma.$disconnect(); +}); + +// Components + Resources +router.get('/:id/resources', async (req: Request, res: Response) => { + const id = req.params.id; + const component = await prisma.component.findUnique({ + where: { + id: id, + }, + include: { + resources: { + orderBy: { + name: 'asc', + }, + }, + }, + }); + + if (!component) { + res.status(404).send('Component not found'); + } else { + res.json(component.resources); + } + + await prisma.$disconnect(); +}); + +router.post('/:id/resources', async (req: Request, res: Response) => { + createAndAddResource(req, res).then(async (resource) => { + res.status(201).json(resource); + await prisma.$disconnect(); + }).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.put('/:id/resources/:resourceId', async (req: Request, res: Response) => { + addResource(req, res).then(async (component) => { + res.json(component); + await prisma.$disconnect(); + } + ).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.put('/:id/resources', async (req: Request, res: Response) => { + const resources = req.body; + const id = req.params.id; + + const component = await prisma.component.update({ + where: { + id: id, + }, + data: { + resources: { + deleteMany: {}, + create: resources, + }, + }, + }); + + res.json(component); + await prisma.$disconnect(); +}); + +router.delete('/:id/resources/:resourceId', async (req: Request, res: Response) => { + removeResource(req, res).then(async (component) => { + res.json(component); + await prisma.$disconnect(); + }).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +const componentsRouter = router; +export default componentsRouter; \ No newline at end of file diff --git a/api/src/components/validation-rules.ts b/api/src/components/validation-rules.ts new file mode 100644 index 000000000..82444cf00 --- /dev/null +++ b/api/src/components/validation-rules.ts @@ -0,0 +1,19 @@ +import { body } from 'express-validator'; + +const commonRules = [ + body('name').notEmpty().withMessage('Name is required.'), +]; + +const createRules = [ + ...commonRules, +]; + +const updateRules = [ + body('id').notEmpty().withMessage('ID is required.'), + ...commonRules, +]; + +export { + createRules, + updateRules, +}; \ No newline at end of file diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 000000000..57acdf812 --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,40 @@ +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import routes from './routes'; +import { logger } from './utils/logger'; + +const app = express(); +const port = process.env.PORT || 8000; + +// Log all requests +app.use((req: Request, res: Response, next: NextFunction) => { + console.log(logger(req, res, {body: true})); + next(); +}); + +// Middleware +app.use(cors()) +app.use(express.json()); + +// Authenticaion middleware +app.use((req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers.authorization; + const token = authHeader?.split(' ')[1]; + + // TODO: Implement authentication middleware + next(); +}); + +// Routes +app.use('/', routes); + +// Error handling +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send('Something\'s not quite right! Error: ' + err.message); +}); + +// Start app server +app.listen(port, () => { + console.log(`Express app server listening at http://localhost:${port}`); +}); diff --git a/api/src/posts/routes.ts b/api/src/posts/routes.ts new file mode 100644 index 000000000..7a6934017 --- /dev/null +++ b/api/src/posts/routes.ts @@ -0,0 +1,101 @@ +import { Router, Request, Response } from 'express'; +import { body, validationResult } from 'express-validator'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); +const router = Router(); + +const postValidationRules = [ + body('title').notEmpty().withMessage('Title is required.'), +]; + +const createPostValidationRules = [ + body('authorId').notEmpty().withMessage('Author ID is required.'), + ...postValidationRules, +]; + +const updatePostValidationRules = [ + body('id').notEmpty().withMessage('ID is required.'), + ...postValidationRules, +]; + +const createPost = async (req: Request, res: Response) => { + const post = await prisma.post.create({ + data: { + title: req.body.title, + content: req.body.content, + author: { + connect: { + id: req.body.authorId, + }, + }, + }, + }); + + return post; +} + +router.post('/', createPostValidationRules, async (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + createPost(req, res).then(async (post) => { + res.status(201).json(post); + await prisma.$disconnect(); + }).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.get('/', async (req: Request, res: Response) => { + const posts = await prisma.post.findMany(); + res.json(posts); + await prisma.$disconnect(); +}); + +router.get('/:id', async (req: Request, res: Response) => { + const id = req.params.id; + const post = await prisma.post.findUnique({ + where: { + id: id, + }, + }); + + res.json(post); + await prisma.$disconnect(); +}); + +router.put('/', updatePostValidationRules, async (req: Request, res: Response) => { + const post = await prisma.post.update({ + where: { + id: req.body.id, + }, + data: { + title: req.body.title, + content: req.body.content, + }, + }); + + res.json(post); + await prisma.$disconnect(); +}); + +router.delete('/:id', async (req: Request, res: Response) => { + const id = req.params.id; + const post = await prisma.post.delete({ + where: { + id: id, + }, + }); + + res.json(post); + await prisma.$disconnect(); +}); + +const postsRouter = router; +export default postsRouter; + diff --git a/api/src/resources/actions.ts b/api/src/resources/actions.ts new file mode 100644 index 000000000..bb2725c05 --- /dev/null +++ b/api/src/resources/actions.ts @@ -0,0 +1,62 @@ +import { PrismaClient } from '@prisma/client'; +import { Request, Response } from 'express'; + +const prisma = new PrismaClient(); + +const createResource = async (req: Request, res: Response) => { + const { name, type, url } = req.body; + + const resource = await prisma.resource.create({ + data: { + name, + type, + url, + }, + }); + + return res.json(resource); +} + +const updateResource = async (req: Request, res: Response) => { + const { id, name, type, url } = req.body; + + const resource = await prisma.resource.update({ + where: { + id: id, + }, + data: { + name, + type, + url, + }, + }); + + return res.json(resource); +} + +const updateResources = async (req: Request, res: Response) => { + const resources = req.body; + + const updatedResources = await Promise.all(resources.map( + async (resource: { id: string, name: string, type: string, url: string}) => { + const { id, name, type, url } = resource; + + const updatedResource = await prisma.resource.update({ + where: { + id: id, + }, + data: { + name, + type, + url, + }, + }); + + return updatedResource; + } + )); + + return res.json([...updatedResources]); +} + +export { createResource, updateResource, updateResources } \ No newline at end of file diff --git a/api/src/resources/routes.ts b/api/src/resources/routes.ts new file mode 100644 index 000000000..93c6dc851 --- /dev/null +++ b/api/src/resources/routes.ts @@ -0,0 +1,90 @@ +import { Router, Request, Response } from 'express'; +import { validationResult } from 'express-validator'; +import { PrismaClient } from '@prisma/client'; +import { createResource, updateResource, updateResources } from './actions'; +import { createRules, updateRules, updateManyRules } from './validation-rules'; + +const prisma = new PrismaClient(); +const router = Router(); + +// Resources CRUD +router.post('/', createRules, async (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + createResource(req, res).then(async (resource) => { + res.status(201).json(resource); + await prisma.$disconnect(); + } + ).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.get('/', async (req: Request, res: Response) => { + const resources = await prisma.resource.findMany({ + orderBy: { + name: 'asc', + }, + }); + res.json(resources); + await prisma.$disconnect(); +}); + +router.get('/:id', async (req: Request, res: Response) => { + const id = req.params.id; + const resource = await prisma.resource.findUnique({ + where: { + id: id, + }, + }); + + if (!resource) { + res.status(404).send('Resource not found'); + } else { + res.json(resource); + } + + await prisma.$disconnect(); +}); + +router.put('/:id', updateRules, async (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + updateResource(req, res).then(async (resource) => { + res.json(resource); + await prisma.$disconnect(); + }).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.put('/', updateManyRules, async (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + updateResources(req, res).then(async (resources) => { + res.json(resources); + await prisma.$disconnect(); + } + ).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + + +const resourcesRouter = router; +export default resourcesRouter; diff --git a/api/src/resources/validation-rules.ts b/api/src/resources/validation-rules.ts new file mode 100644 index 000000000..75f244f7f --- /dev/null +++ b/api/src/resources/validation-rules.ts @@ -0,0 +1,25 @@ +import { body } from 'express-validator'; + +const commonRules = [ + body('name').notEmpty().withMessage('Name is required.'), +]; + +const createRules = [ + ...commonRules, +]; + +const updateRules = [ + ...commonRules, +]; + +const updateManyRules = [ + body().isArray().withMessage('Body must be an array.'), + body('*.id').notEmpty().withMessage('Id is required.'), + body('*.name').notEmpty().withMessage('Name is required.'), +]; + +export { + createRules, + updateRules, + updateManyRules, +}; \ No newline at end of file diff --git a/api/src/routes.ts b/api/src/routes.ts new file mode 100644 index 000000000..0b1830c2b --- /dev/null +++ b/api/src/routes.ts @@ -0,0 +1,20 @@ +import { Router, Request, Response } from 'express'; +import componentsRouter from './components/routes'; +import postsRouter from './posts/routes'; +import resourcesRouter from './resources/routes'; +import tasksRouter from './tasks/routes'; +import usersRouter from './users/routes'; + +const router = Router(); + +router.get('/', (req: Request, res: Response) => { + res.send('Hello World'); +}); + +router.use('/components', componentsRouter); +router.use('/posts', postsRouter); +router.use('/resources', resourcesRouter); +router.use('/tasks', tasksRouter); +router.use('/users', usersRouter); + +export default router; diff --git a/api/src/tasks/model.ts b/api/src/tasks/model.ts new file mode 100644 index 000000000..b9ec412b1 --- /dev/null +++ b/api/src/tasks/model.ts @@ -0,0 +1,6 @@ +export interface Task { + id: number; + title: string; + description: string; + completed: boolean; +} diff --git a/api/src/tasks/routes.ts b/api/src/tasks/routes.ts new file mode 100644 index 000000000..22f7b208e --- /dev/null +++ b/api/src/tasks/routes.ts @@ -0,0 +1,80 @@ +import { Router, Request, Response } from 'express'; +import { body, validationResult } from 'express-validator'; +import { Task } from './model' + +const router = Router(); +let tasks: Task[] = []; + +const taskValidationRules = [ + body('title').notEmpty().isString().withMessage('Title is required.'), + body('description').isString(), + body('completed').isBoolean().withMessage('Completed must be either true or false.'), +]; + +router.post('/', taskValidationRules, (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + const task: Task = { + id: tasks.length + 1, + title: req.body.title, + description: req.body.description, + completed: false, + }; + + tasks.push(task); + res.status(201).json(task); +}); + +router.get('/', (req: Request, res: Response) => { + res.json(tasks); +}); + +router.get('/:id', (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const task = tasks.find((task) => task.id === id); + + if (!task) { + res.status(404).send('Task not found'); + } else { + res.json(task); + } +}); + +router.put('/:id', taskValidationRules, (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const task = tasks.find((task) => task.id === id); + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + if (!task) { + res.status(404).send('Task not found'); + } else { + task.title = req.body.title || task.title; + task.description = req.body.description || task.description; + task.completed = req.body.completed || task.completed; + + res.json(task); + } +}); + +router.delete('/:id', (req: Request, res: Response) => { + const id = parseInt(req.params.id); + const taskIndex = tasks.findIndex((task) => task.id === id); + + if (taskIndex === -1) { + res.status(404).send('Task not found'); + } else { + tasks.splice(taskIndex, 1); + res.status(204).send(); + } +}); + +const tasksRouter = router; +export default tasksRouter; diff --git a/api/src/users/routes.ts b/api/src/users/routes.ts new file mode 100644 index 000000000..1990c5a5d --- /dev/null +++ b/api/src/users/routes.ts @@ -0,0 +1,106 @@ +import { Router, Request, Response } from 'express'; +import { body, validationResult } from 'express-validator'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); +const router = Router(); + +const userValidationRules = [ + body('email').optional().trim().isEmail().withMessage('Email must be a valid email address.'), +]; + +const createUserValidationRules = [ + body('email').notEmpty().withMessage('Email is required.'), + ...userValidationRules, +]; + +const updateUserValidationRules = [ + body('id').notEmpty().withMessage('ID is required.'), + ...userValidationRules, +]; + +const createUser = async (req: Request, res: Response) => { + const user = await prisma.user.create({ + data: { + email: req.body.email, + name: req.body.name + }, + }); + + return user; +} + +router.post('/', createUserValidationRules, async (req: Request, res: Response) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + createUser(req, res).then(async (user) => { + res.status(201).json(user); + await prisma.$disconnect(); + }).catch(async (error) => { + console.error(error); + await prisma.$disconnect(); + }); +}); + +router.get('/', async (req: Request, res: Response) => { + const users = await prisma.user.findMany(); + res.json(users); + await prisma.$disconnect(); +}); + +router.get('/:id', async (req: Request, res: Response) => { + const { id } = req.params; + + const user = await prisma.user.findUnique({ + where: { + id: id, + }, + }); + + if (!user) { + res.status(404).send('User not found'); + } else { + res.json(user); + } + + await prisma.$disconnect(); +}); + +router.put('/:id', updateUserValidationRules, async (req: Request, res: Response) => { + const id = req.params.id; + const user = await prisma.user.findUnique({ + where: { + id: id, + }, + }); + const errors = validationResult(req); + + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.array() }); + } + + if (!user) { + res.status(404).send('User not found'); + } else { + const updatedUser = await prisma.user.update({ + where: { + id: id, + }, + data: { + email: req.body.email || user.email, + name: req.body.name || user.name + }, + }); + + res.json(updatedUser); + } + + await prisma.$disconnect(); +}); + +const usersRouter = router; +export default usersRouter; \ No newline at end of file diff --git a/api/src/utils/logger.ts b/api/src/utils/logger.ts new file mode 100644 index 000000000..8229caccd --- /dev/null +++ b/api/src/utils/logger.ts @@ -0,0 +1,33 @@ +import exp from 'constants'; +import { Request, Response } from 'express'; + +export interface LoggerOptions { + method?: boolean; + url?: boolean; + headers?: boolean; + body?: boolean; + baseUrl?: boolean; + originalUrl?: boolean; + path?: boolean; + query?: boolean; + params?: boolean; + referer?: boolean; +} + +export const logger = (req: Request, res: Response, options: LoggerOptions) => { + let log = ''; + log += `${req.method} ${req.url}\n`; + + if(options.method) log += `Method: ${req.method}\n`; + if(options.url) log += `URL: ${req.url}\n`; + if(options.headers) log += `Headers: ${req.headers}\n`; + if(options.body) log += `Body: ${req.body}\n`; + if(options.baseUrl) log += `BaseUrl: ${req.baseUrl}\n`; + if(options.originalUrl) log += `OriginalUrl: ${req.originalUrl}\n`; + if(options.path) log += `Path: ${req.path}\n`; + if(options.query) log += `Query: ${req.query}\n`; + if(options.params) log += `Params: ${req.params}\n`; + if(options.referer) log += `Referrer: ${req.headers.referer}\n`; + + return log; +} diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 000000000..deaf0d37c --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/aries-core/src/js/components/controls/ReverseAnchor.js b/aries-core/src/js/components/controls/ReverseAnchor.js new file mode 100644 index 000000000..830af452d --- /dev/null +++ b/aries-core/src/js/components/controls/ReverseAnchor.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Anchor } from 'grommet'; +import { FormPrevious } from 'grommet-icons'; + +export const ReverseAnchor = React.forwardRef( + ({ href, label, ...rest }, ref) => { + return ( + } + gap="hair" + {...rest} + /> + ); + }, +); + +ReverseAnchor.propTypes = { + href: PropTypes.string, + label: PropTypes.string.isRequired, +}; diff --git a/aries-core/src/js/components/controls/index.d.ts b/aries-core/src/js/components/controls/index.d.ts new file mode 100644 index 000000000..bcfd44751 --- /dev/null +++ b/aries-core/src/js/components/controls/index.d.ts @@ -0,0 +1,8 @@ +export interface ReverseAnchorProps { + href?: string; + label: string; +} + +declare const ReverseAnchor: React.FC; + +export { ReverseAnchor }; diff --git a/aries-core/src/js/components/controls/index.js b/aries-core/src/js/components/controls/index.js new file mode 100644 index 000000000..343e37938 --- /dev/null +++ b/aries-core/src/js/components/controls/index.js @@ -0,0 +1 @@ +export * from './ReverseAnchor'; diff --git a/aries-core/src/js/components/index.js b/aries-core/src/js/components/index.js index e73559006..f66549e05 100644 --- a/aries-core/src/js/components/index.js +++ b/aries-core/src/js/components/index.js @@ -1,3 +1,4 @@ +export * from './controls'; export * from './core'; export * from './helpers'; export * from './layouts'; diff --git a/aries-core/src/js/components/layouts/ButtonGroup/ButtonGroup.js b/aries-core/src/js/components/layouts/ButtonGroup/ButtonGroup.js index 651edf442..ac94b391d 100644 --- a/aries-core/src/js/components/layouts/ButtonGroup/ButtonGroup.js +++ b/aries-core/src/js/components/layouts/ButtonGroup/ButtonGroup.js @@ -6,7 +6,7 @@ export const ButtonGroup = ({ children, ...rest }) => { const { buttonGroup } = useContext(ThemeContext); return ( - + {children} ); diff --git a/aries-core/src/js/components/layouts/FormChildObjects/ChildHeader.jsx b/aries-core/src/js/components/layouts/FormChildObjects/ChildHeader.jsx new file mode 100644 index 000000000..391c66674 --- /dev/null +++ b/aries-core/src/js/components/layouts/FormChildObjects/ChildHeader.jsx @@ -0,0 +1,57 @@ +// ChildHeader.js +import { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Box, Header, Heading, Text } from 'grommet'; +import { Up, Down } from 'grommet-icons'; + +export const ChildHeader = ({ + collectionName, + index, + headingLevel, + name, + open, + summary, + ...rest +}) => { + const [background, setBackground] = useState(null); + const borderStyle = { side: 'top', color: 'border-weak' }; + + return ( +
setBackground('background-contrast')} + onMouseLeave={() => setBackground(null)} + pad="small" + {...rest} + > + + + {name || `New ${collectionName} (undefined)`} + + {summary && {summary}} + + {open ? ( + + ) : ( + + )} +
+ ); +}; + +ChildHeader.propTypes = { + annotationIds: PropTypes.shape({ + container: PropTypes.string, + label: PropTypes.string, + icon: PropTypes.string, + valuesSummary: PropTypes.string, + }), + collectionName: PropTypes.string, + index: PropTypes.number, + headingLevel: PropTypes.number, + name: PropTypes.string, + open: PropTypes.bool, + summary: PropTypes.string, +}; diff --git a/aries-core/src/js/components/layouts/FormChildObjects/FormChildObject.jsx b/aries-core/src/js/components/layouts/FormChildObjects/FormChildObject.jsx new file mode 100644 index 000000000..f73859881 --- /dev/null +++ b/aries-core/src/js/components/layouts/FormChildObjects/FormChildObject.jsx @@ -0,0 +1,96 @@ +// FormChildObject.js +import { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Box, Button, Collapsible } from 'grommet'; +import { Trash } from 'grommet-icons'; +import { ChildHeader } from './ChildHeader'; + +// using value names from summarize prop, builds the summary message +// to be displayed beneath the heading +const getSummaryString = (values, keys) => { + let summary = ''; + const summarize = new Map(); + for (let i = 0; i <= keys.length - 1; i += 1) { + if (typeof keys[i] === 'object') { + summarize.set(keys[i].name, keys[i].showName !== false); + } else { + summarize.set(keys[i], true); + } + } + + // create flat array of just keys from Map + const flattenedKeys = [...summarize.keys()]; + Object.entries(values).forEach(([key, value]) => { + summary += + flattenedKeys.includes(key) && + (value.length > 0 || typeof value === 'number') + ? // if the showName value for a key is true, include it + `${summarize.get(key) ? `${key}: ` : ''}${value}, ` + : ''; + }); + summary = summary.slice(0, -2); + return summary; +}; + +export const FormChildObject = ({ + children, + collectionName, + index, + headingLevel, + name, + onClick: onClickProp, + onRemove, + open: openProp = false, + summarize, + values, +}) => { + const [open, setOpen] = useState(openProp); + const valuesSummary = summarize ? getSummaryString(values, summarize) : null; + const onClick = () => onClickProp || setOpen(!open); + + return ( + <> + + + {children} + + {onRemove && ( +