diff --git a/utopia-remix/app/models/project.server.ts b/utopia-remix/app/models/project.server.ts index e8a008147689..b0f282299371 100644 --- a/utopia-remix/app/models/project.server.ts +++ b/utopia-remix/app/models/project.server.ts @@ -3,18 +3,32 @@ import { prisma } from '../db.server' export type ProjectWithoutContent = Omit +const selectProjectWithoutContent: Record = { + id: true, + proj_id: true, + owner_id: true, + title: true, + created_at: true, + modified_at: true, + deleted: true, +} + export async function listProjects(params: { ownerId: string }): Promise { return prisma.project.findMany({ - select: { - id: true, - proj_id: true, - owner_id: true, - title: true, - created_at: true, - modified_at: true, - deleted: true, - }, + select: selectProjectWithoutContent, where: { owner_id: params.ownerId }, orderBy: { modified_at: 'desc' }, }) } + +export async function renameProject(params: { + id: string + userId: string + title: string +}): Promise { + return prisma.project.update({ + where: { proj_id: params.id, owner_id: params.userId }, + data: { title: params.title }, + select: selectProjectWithoutContent, + }) +} diff --git a/utopia-remix/app/routes-test/projects.$id.rename.spec.ts b/utopia-remix/app/routes-test/projects.$id.rename.spec.ts new file mode 100644 index 000000000000..287bfb059df7 --- /dev/null +++ b/utopia-remix/app/routes-test/projects.$id.rename.spec.ts @@ -0,0 +1,92 @@ +import { prisma } from '../db.server' +import { + createTestProject, + createTestSession, + createTestUser, + newFormData, + newTestRequest, + truncateTables, +} from '../test-util' +import { ApiError } from '../util/api.server' +import { handleRenameProject } from '../routes/projects.$id.rename' + +describe('handleRenameProject', () => { + afterEach(async () => { + await truncateTables([ + prisma.userDetails, + prisma.persistentSession, + prisma.project, + prisma.projectID, + ]) + }) + + beforeEach(async () => { + await createTestUser(prisma, { id: 'foo' }) + await createTestUser(prisma, { id: 'bar' }) + await createTestSession(prisma, { key: 'the-key', userId: 'foo' }) + await createTestProject(prisma, { id: 'one', ownerId: 'foo', title: 'project-one' }) + await createTestProject(prisma, { id: 'two', ownerId: 'bar', title: 'project-two' }) + }) + + it('requires a POST method', async () => { + const fn = async () => handleRenameProject(newTestRequest(), {}) + await expect(fn).rejects.toThrow(ApiError) + await expect(fn).rejects.toThrow('invalid method') + }) + it('requires a user', async () => { + const fn = async () => + handleRenameProject(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {}) + await expect(fn).rejects.toThrow(ApiError) + await expect(fn).rejects.toThrow('session not found') + }) + it('requires a valid id', async () => { + const fn = async () => + handleRenameProject(newTestRequest({ method: 'POST', authCookie: 'the-key' }), {}) + await expect(fn).rejects.toThrow(ApiError) + await expect(fn).rejects.toThrow('id is null') + }) + it('requires a valid title', async () => { + const fn = (data: FormData) => async () => { + const req = newTestRequest({ method: 'POST', authCookie: 'the-key', formData: data }) + return handleRenameProject(req, { id: 'project-one' }) + } + + await expect(fn(newFormData({}))).rejects.toThrow(ApiError) + await expect(fn(newFormData({}))).rejects.toThrow('title is null') + + await expect(fn(newFormData({ title: '' }))).rejects.toThrow(ApiError) + await expect(fn(newFormData({ title: '' }))).rejects.toThrow('title is too short') + }) + it('requires a valid project', async () => { + const fn = (data: FormData) => async () => { + const req = newTestRequest({ method: 'POST', authCookie: 'the-key', formData: data }) + return handleRenameProject(req, { id: 'doesnt-exist' }) + } + + await expect(fn(newFormData({ title: 'hello' }))).rejects.toThrow('Record to update not found') + }) + it('requires ownership of the project', async () => { + const fn = (data: FormData) => async () => { + const req = newTestRequest({ method: 'POST', authCookie: 'the-key', formData: data }) + return handleRenameProject(req, { id: 'two' }) + } + + await expect(fn(newFormData({ title: 'hello' }))).rejects.toThrow('Record to update not found') + }) + it('renames the project', async () => { + const fn = async (data: FormData) => { + const req = newTestRequest({ method: 'POST', authCookie: 'the-key', formData: data }) + return handleRenameProject(req, { id: 'one' }) + } + + await fn(newFormData({ title: 'hello' })) + let project = await prisma.project.findFirst({ where: { proj_id: 'one' } }) + + expect(project?.title).toEqual('hello') + + await fn(newFormData({ title: 'hello AGAIN!' })) + project = await prisma.project.findFirst({ where: { proj_id: 'one' } }) + + expect(project?.title).toEqual('hello-again') + }) +}) diff --git a/utopia-remix/app/routes/projects.$id.rename.tsx b/utopia-remix/app/routes/projects.$id.rename.tsx new file mode 100644 index 000000000000..c489ae083c64 --- /dev/null +++ b/utopia-remix/app/routes/projects.$id.rename.tsx @@ -0,0 +1,34 @@ +import { ActionFunctionArgs } from '@remix-run/node' +import { renameProject } from '../models/project.server' +import { ensure, handle, requireUser } from '../util/api.server' +import slugify from 'slugify' +import { Status } from '../util/statusCodes.server' +import { Params } from '@remix-run/react' + +export const SLUGIFY_OPTIONS = { lower: true, remove: /[^a-z0-9A-Z ]/ } + +export async function action(args: ActionFunctionArgs) { + return handle(args, { POST: handleRenameProject }) +} + +export async function handleRenameProject(req: Request, params: Params) { + ensure(req.method === 'POST', 'invalid method', Status.METHOD_NOT_ALLOWED) + + const user = await requireUser(req) + + const { id } = params + ensure(id != null, 'id is null', Status.BAD_REQUEST) + + const formData = await req.formData() + + const title = formData.get('title') + ensure(title != null, 'title is null', Status.BAD_REQUEST) + ensure(typeof title === 'string', 'title is not a string', Status.BAD_REQUEST) + + const slug = slugify(title, SLUGIFY_OPTIONS) + ensure(slug.length > 0, 'title is too short', Status.BAD_REQUEST) + + await renameProject({ id: id, userId: user.user_id, title: slug }) + + return {} +} diff --git a/utopia-remix/app/routes/v1.asset.$id.assets.$name.tsx b/utopia-remix/app/routes/v1.asset.$id.assets.$name.tsx index 490d1bba6a33..694757666da1 100644 --- a/utopia-remix/app/routes/v1.asset.$id.assets.$name.tsx +++ b/utopia-remix/app/routes/v1.asset.$id.assets.$name.tsx @@ -3,13 +3,13 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { POST: (req) => proxy(req, { rawOutput: true }), }) } diff --git a/utopia-remix/app/routes/v1.collaboration.tsx b/utopia-remix/app/routes/v1.collaboration.tsx index f16a125ceeb7..a4e6bdd285e1 100644 --- a/utopia-remix/app/routes/v1.collaboration.tsx +++ b/utopia-remix/app/routes/v1.collaboration.tsx @@ -3,13 +3,13 @@ import { handle, handleOptions } from '../util/api.server' import { proxy } from '../util/proxy.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { PUT: proxy, }) } diff --git a/utopia-remix/app/routes/v1.github.authentication.finish.tsx b/utopia-remix/app/routes/v1.github.authentication.finish.tsx index cef37054f22f..286e2ad15532 100644 --- a/utopia-remix/app/routes/v1.github.authentication.finish.tsx +++ b/utopia-remix/app/routes/v1.github.authentication.finish.tsx @@ -3,7 +3,7 @@ import { handle, handleOptions } from '../util/api.server' import { proxy } from '../util/proxy.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.authentication.start.tsx b/utopia-remix/app/routes/v1.github.authentication.start.tsx index cef37054f22f..286e2ad15532 100644 --- a/utopia-remix/app/routes/v1.github.authentication.start.tsx +++ b/utopia-remix/app/routes/v1.github.authentication.start.tsx @@ -3,7 +3,7 @@ import { handle, handleOptions } from '../util/api.server' import { proxy } from '../util/proxy.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.authentication.status.tsx b/utopia-remix/app/routes/v1.github.authentication.status.tsx index cef37054f22f..286e2ad15532 100644 --- a/utopia-remix/app/routes/v1.github.authentication.status.tsx +++ b/utopia-remix/app/routes/v1.github.authentication.status.tsx @@ -3,7 +3,7 @@ import { handle, handleOptions } from '../util/api.server' import { proxy } from '../util/proxy.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.asset.$assetSha.tsx b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.asset.$assetSha.tsx index 832376b6f505..66e84857b569 100644 --- a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.asset.$assetSha.tsx +++ b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.asset.$assetSha.tsx @@ -3,13 +3,13 @@ import { handle, handleOptions } from '../util/api.server' import { proxy } from '../util/proxy.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { POST: (req) => proxy(req, { rawOutput: true }), }) } diff --git a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.pullrequest.tsx b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.pullrequest.tsx index cef37054f22f..286e2ad15532 100644 --- a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.pullrequest.tsx +++ b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.pullrequest.tsx @@ -3,7 +3,7 @@ import { handle, handleOptions } from '../util/api.server' import { proxy } from '../util/proxy.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.tsx b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.tsx +++ b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.$branchName.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.default-branch.tsx b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.default-branch.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.default-branch.tsx +++ b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.branch.default-branch.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.tsx b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.github.branches.$owner.$repository.tsx +++ b/utopia-remix/app/routes/v1.github.branches.$owner.$repository.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.save.$projectId.tsx b/utopia-remix/app/routes/v1.github.save.$projectId.tsx index ca614fd3821b..46737a1af507 100644 --- a/utopia-remix/app/routes/v1.github.save.$projectId.tsx +++ b/utopia-remix/app/routes/v1.github.save.$projectId.tsx @@ -3,13 +3,13 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { POST: proxy, }) } diff --git a/utopia-remix/app/routes/v1.github.user.repositories.tsx b/utopia-remix/app/routes/v1.github.user.repositories.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.github.user.repositories.tsx +++ b/utopia-remix/app/routes/v1.github.user.repositories.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.github.user.tsx b/utopia-remix/app/routes/v1.github.user.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.github.user.tsx +++ b/utopia-remix/app/routes/v1.github.user.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.javascript.package.versions.$name.$version.tsx b/utopia-remix/app/routes/v1.javascript.package.versions.$name.$version.tsx index cef37054f22f..286e2ad15532 100644 --- a/utopia-remix/app/routes/v1.javascript.package.versions.$name.$version.tsx +++ b/utopia-remix/app/routes/v1.javascript.package.versions.$name.$version.tsx @@ -3,7 +3,7 @@ import { handle, handleOptions } from '../util/api.server' import { proxy } from '../util/proxy.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.liveblocks.authentication.tsx b/utopia-remix/app/routes/v1.liveblocks.authentication.tsx index ca614fd3821b..46737a1af507 100644 --- a/utopia-remix/app/routes/v1.liveblocks.authentication.tsx +++ b/utopia-remix/app/routes/v1.liveblocks.authentication.tsx @@ -3,13 +3,13 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { POST: proxy, }) } diff --git a/utopia-remix/app/routes/v1.liveblocks.enabled.tsx b/utopia-remix/app/routes/v1.liveblocks.enabled.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.liveblocks.enabled.tsx +++ b/utopia-remix/app/routes/v1.liveblocks.enabled.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.project.$id.metadata.tsx b/utopia-remix/app/routes/v1.project.$id.metadata.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.project.$id.metadata.tsx +++ b/utopia-remix/app/routes/v1.project.$id.metadata.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.project.$id.owner.tsx b/utopia-remix/app/routes/v1.project.$id.owner.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.project.$id.owner.tsx +++ b/utopia-remix/app/routes/v1.project.$id.owner.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.project.$id.tsx b/utopia-remix/app/routes/v1.project.$id.tsx index 0e127e39ab93..ea397fe1d04f 100644 --- a/utopia-remix/app/routes/v1.project.$id.tsx +++ b/utopia-remix/app/routes/v1.project.$id.tsx @@ -3,14 +3,14 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { PUT: proxy, }) } diff --git a/utopia-remix/app/routes/v1.projectid.tsx b/utopia-remix/app/routes/v1.projectid.tsx index ca614fd3821b..46737a1af507 100644 --- a/utopia-remix/app/routes/v1.projectid.tsx +++ b/utopia-remix/app/routes/v1.projectid.tsx @@ -3,13 +3,13 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { POST: proxy, }) } diff --git a/utopia-remix/app/routes/v1.projects.tsx b/utopia-remix/app/routes/v1.projects.tsx index 61233faaf027..0347265a1f8a 100644 --- a/utopia-remix/app/routes/v1.projects.tsx +++ b/utopia-remix/app/routes/v1.projects.tsx @@ -3,7 +3,7 @@ import { handleListProjects } from '../handlers/listProjects' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: handleListProjects, }) diff --git a/utopia-remix/app/routes/v1.showcase.tsx b/utopia-remix/app/routes/v1.showcase.tsx index 8030a6219e61..a8110da1e115 100644 --- a/utopia-remix/app/routes/v1.showcase.tsx +++ b/utopia-remix/app/routes/v1.showcase.tsx @@ -3,7 +3,7 @@ import { handle, handleOptions } from '../util/api.server' import { ListProjectsResponse } from '../types' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: handleShowcase, }) diff --git a/utopia-remix/app/routes/v1.thumbnail.$projectId.tsx b/utopia-remix/app/routes/v1.thumbnail.$projectId.tsx index 3eea83f0cca8..8b4419fece6b 100644 --- a/utopia-remix/app/routes/v1.thumbnail.$projectId.tsx +++ b/utopia-remix/app/routes/v1.thumbnail.$projectId.tsx @@ -4,14 +4,14 @@ import { ensure, handle, handleOptions } from '../util/api.server' import { Params } from '@remix-run/react' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: handleGetThumbnail(args.params), }) } export async function action(args: ActionFunctionArgs) { - return handle(args.request, { + return handle(args, { POST: proxy, }) } diff --git a/utopia-remix/app/routes/v1.user.config.tsx b/utopia-remix/app/routes/v1.user.config.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.user.config.tsx +++ b/utopia-remix/app/routes/v1.user.config.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/routes/v1.user.tsx b/utopia-remix/app/routes/v1.user.tsx index 10a0fba22ba8..06eb55c3221b 100644 --- a/utopia-remix/app/routes/v1.user.tsx +++ b/utopia-remix/app/routes/v1.user.tsx @@ -3,7 +3,7 @@ import { proxy } from '../util/proxy.server' import { handle, handleOptions } from '../util/api.server' export async function loader(args: LoaderFunctionArgs) { - return handle(args.request, { + return handle(args, { OPTIONS: handleOptions, GET: proxy, }) diff --git a/utopia-remix/app/test-util.ts b/utopia-remix/app/test-util.ts index 08fc486091e6..16975edd451a 100644 --- a/utopia-remix/app/test-util.ts +++ b/utopia-remix/app/test-util.ts @@ -23,6 +23,7 @@ export async function createTestProject( params: { id: string ownerId: string + title?: string content?: string createdAt?: Date modifiedAt?: Date @@ -35,7 +36,7 @@ export async function createTestProject( await client.project.create({ data: { proj_id: params.id, - title: params.id, + title: params.title ?? params.id, owner_id: params.ownerId, created_at: params.createdAt ?? now, modified_at: params.modifiedAt ?? now, @@ -77,11 +78,16 @@ export async function truncateTables(models: DeletableModel[]) { export function newTestRequest(params?: { path?: string + method?: string headers?: { [key: string]: string } authCookie?: string + formData?: FormData }): Request { const path = (params?.path ?? '').replace(/^\/+/, '') - const req = new Request(`http://localhost:8002/` + path) + const req = new Request(`http://localhost:8002/` + path, { + method: params?.method, + body: params?.formData, + }) if (params?.headers != null) { for (const key of Object.keys(params.headers)) { @@ -95,3 +101,11 @@ export function newTestRequest(params?: { return req } + +export function newFormData(data: { [key: string]: string }): FormData { + const formData = new FormData() + for (const key of Object.keys(data)) { + formData.append(key, data[key]) + } + return formData +} diff --git a/utopia-remix/app/util/api.server.ts b/utopia-remix/app/util/api.server.ts index a3a746d29531..a34bc259ecd1 100644 --- a/utopia-remix/app/util/api.server.ts +++ b/utopia-remix/app/util/api.server.ts @@ -6,6 +6,8 @@ import { Method } from './methods.server' import { UserDetails } from 'prisma-client' import { getUserFromSession } from '../models/session.server' import * as cookie from 'cookie' +import { Params } from '@remix-run/react' +import { PrismaClientKnownRequestError } from 'prisma-client/runtime/library.js' interface ErrorResponse { error: string @@ -26,25 +28,31 @@ export async function handleOptions(): Promise> { return json({}, { headers: responseHeaders }) } +interface HandleRequest { + request: Request + params: Params +} + export function handle( - request: Request, + { request, params }: HandleRequest, handlers: { - [method in Method]?: (request: Request) => Promise + [method in Method]?: (request: Request, params: Params) => Promise }, ): Promise { const handler = handlers[request.method as Method] if (handler == null) { throw new ApiError('invalid method', Status.METHOD_NOT_ALLOWED) } - return handleMethod(request, handler) + return handleMethod(request, params, handler) } async function handleMethod( request: Request, - fn: (request: Request) => Promise, + params: Params, + fn: (request: Request, params: Params) => Promise, ): Promise | unknown> { try { - const resp = await fn(request) + const resp = await fn(request, params) if (resp instanceof Response) { return new Response(resp.body, { headers: { @@ -55,10 +63,7 @@ async function handleMethod( } return json(resp, { headers: responseHeaders }) } catch (err) { - const isApiError = err instanceof ApiError - const message = isApiError ? err.message : `${err}` - const status = isApiError ? err.status : 500 - const name = isApiError ? err.name : 'Error' + const { message, status, name } = getErrorData(err) console.error(`${request.method} ${request.url}: ${message}`) @@ -69,6 +74,28 @@ async function handleMethod( } } +function getErrorData(err: unknown): { message: string; status: number; name: string } { + if (err instanceof ApiError) { + return { + message: err.message, + status: err.status, + name: err.name, + } + } else if (err instanceof PrismaClientKnownRequestError) { + return { + message: (err.meta?.cause as string) ?? err.message, + status: Status.INTERNAL_ERROR, + name: (err.meta?.modelName as string) ?? err.name, + } + } else { + return { + message: `${err}`, + status: Status.INTERNAL_ERROR, + name: 'Error', + } + } +} + export class ApiError extends Error { status: number constructor(message: string, code: number) { diff --git a/utopia-remix/app/util/slugify.spec.ts b/utopia-remix/app/util/slugify.spec.ts new file mode 100644 index 000000000000..134c2a7c4f34 --- /dev/null +++ b/utopia-remix/app/util/slugify.spec.ts @@ -0,0 +1,53 @@ +import slugify from 'slugify' +import { SLUGIFY_OPTIONS } from '../routes/projects.$id.rename' + +describe('slugify', () => { + const tests: { name: string; input: string; wanted: string }[] = [ + { + name: 'a single word', + input: 'Hello', + wanted: 'hello', + }, + { + name: 'alphanumeric', + input: 'the Answer is 42', + wanted: 'the-answer-is-42', + }, + { + name: 'multiple words, mixed case', + input: 'Hello WoRld', + wanted: 'hello-world', + }, + { + name: 'multiple words with trailing non alphanumeric', + input: '¡hello WORLD!', + wanted: 'hello-world', + }, + { + name: 'multiple spaces', + input: 'some large spacing!!', + wanted: 'some-large-spacing', + }, + { + name: 'empty string', + input: '', + wanted: '', + }, + { + name: 'uppercase', + input: 'ALLCAPS', + wanted: 'allcaps', + }, + { + name: 'only non alphanumeric', + input: '!!?!', + wanted: '', + }, + ] + for (let i = 0; i < tests.length; i++) { + const test = tests[i] + it(`${i + 1}/${tests.length} ${test.name}`, async () => { + expect(slugify(test.input, SLUGIFY_OPTIONS)).toEqual(test.wanted) + }) + } +}) diff --git a/utopia-remix/package.json b/utopia-remix/package.json index e87236b7c67e..1b8f8964183c 100644 --- a/utopia-remix/package.json +++ b/utopia-remix/package.json @@ -26,6 +26,7 @@ "prisma-client": "link:node_modules/@utopia/prisma-client", "react": "18.2.0", "react-dom": "18.2.0", + "slugify": "1.6.6", "tiny-invariant": "1.3.1", "url-join": "5.0.0" }, diff --git a/utopia-remix/pnpm-lock.yaml b/utopia-remix/pnpm-lock.yaml index 30959007da51..c2c7f4f3b1ec 100644 --- a/utopia-remix/pnpm-lock.yaml +++ b/utopia-remix/pnpm-lock.yaml @@ -36,6 +36,7 @@ specifiers: prisma-client: link:node_modules/@utopia/prisma-client react: 18.2.0 react-dom: 18.2.0 + slugify: 1.6.6 tiny-invariant: 1.3.1 ts-node: 10.9.2 typescript: 5.1.6 @@ -54,6 +55,7 @@ dependencies: prisma-client: link:node_modules/@utopia/prisma-client react: 18.2.0 react-dom: 18.2.0_react@18.2.0 + slugify: 1.6.6 tiny-invariant: 1.3.1 url-join: 5.0.0 @@ -7769,6 +7771,11 @@ packages: engines: {node: '>=8'} dev: true + /slugify/1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + dev: false + /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'}