diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts index 2e6856168..3da8863f0 100644 --- a/apps/backend/src/modules/auth/auth.ts +++ b/apps/backend/src/modules/auth/auth.ts @@ -231,7 +231,7 @@ export class Auth { }) .execute() - const space = await this.spaceService.createPersonalSpace() + const space = await this.spaceService.createPersonalSpace(username!) await this.spaceMemberService.createMember(userId, space.id.value, "owner") spaceId = space.id.value @@ -296,7 +296,7 @@ export class Auth { let space = await this.spaceService.getSpace({ userId: user.id }) if (space.isSome()) { } else { - space = Some(await this.spaceService.createPersonalSpace()) + space = Some(await this.spaceService.createPersonalSpace(user.username)) await this.spaceMemberService.createMember(user.id, space.unwrap().id.value, "owner") } const session = await this.lucia.createSession(user.id, { space_id: space.unwrap().id.value }) diff --git a/apps/backend/src/modules/web/web.tsx b/apps/backend/src/modules/web/web.tsx index 68e00b6ee..c90edd15b 100644 --- a/apps/backend/src/modules/web/web.tsx +++ b/apps/backend/src/modules/web/web.tsx @@ -14,7 +14,7 @@ export class Web { .get("/s/*", () => index) .get("/bases/*", () => index) .get("/account/*", () => index) - .get("/members", () => index) + .get("/settings", () => index) .get("/login", () => index) .get("/signup", () => index) } diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index bb667da87..1f6de8896 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -133,6 +133,7 @@ enum ShareTargetType { } type Space { + avatar: String id: ID! isPersonal: Boolean! member: SpaceMember diff --git a/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte b/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte index cea3ab8bb..e475292c8 100644 --- a/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte +++ b/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte @@ -1,6 +1,6 @@ + + + + + + Space name + + + Change space display name. + + + + Update + {#if browser} + + {/if} + + diff --git a/apps/frontend/src/routes/(authed)/+layout.gql b/apps/frontend/src/routes/(authed)/+layout.gql index 4ee18a292..f006ff1bb 100644 --- a/apps/frontend/src/routes/(authed)/+layout.gql +++ b/apps/frontend/src/routes/(authed)/+layout.gql @@ -3,6 +3,7 @@ query GetIndexQuery { id name isPersonal + avatar } tables { diff --git a/apps/frontend/src/routes/(authed)/members/+page.svelte b/apps/frontend/src/routes/(authed)/members/+page.svelte deleted file mode 100644 index 093ec5506..000000000 --- a/apps/frontend/src/routes/(authed)/members/+page.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - Members - - - - - - - {#if $hasPermission("authz:invite")} - - {/if} - {#if $hasPermission("authz:listInvitation")} - - {/if} - - - - - diff --git a/apps/frontend/src/routes/(authed)/members/+layout.gql b/apps/frontend/src/routes/(authed)/settings/+layout.gql similarity index 100% rename from apps/frontend/src/routes/(authed)/members/+layout.gql rename to apps/frontend/src/routes/(authed)/settings/+layout.gql diff --git a/apps/frontend/src/routes/(authed)/members/+layout.ts b/apps/frontend/src/routes/(authed)/settings/+layout.ts similarity index 100% rename from apps/frontend/src/routes/(authed)/members/+layout.ts rename to apps/frontend/src/routes/(authed)/settings/+layout.ts diff --git a/apps/frontend/src/routes/(authed)/settings/+page.svelte b/apps/frontend/src/routes/(authed)/settings/+page.svelte new file mode 100644 index 000000000..07347ea35 --- /dev/null +++ b/apps/frontend/src/routes/(authed)/settings/+page.svelte @@ -0,0 +1,79 @@ + + + + + + + Settings + + + + + + + + Members + + + + Settings + + + + + + + Members + + + {#if $hasPermission("authz:invite")} + + {/if} + {#if $hasPermission("authz:listInvitation")} + + {/if} + + + + + + + + {#if space} + + {/if} + + + diff --git a/packages/command-handlers/src/handlers/index.ts b/packages/command-handlers/src/handlers/index.ts index 1594f2777..fbe487150 100644 --- a/packages/command-handlers/src/handlers/index.ts +++ b/packages/command-handlers/src/handlers/index.ts @@ -35,6 +35,7 @@ import { SetViewSortCommandHandler } from "./set-view-sort.command-handler" import { UpdateAccountCommandHandler } from "./update-account.command-handler" import { UpdateBaseCommandHandler } from "./update-base.command-handler" import { UpdateRecordCommandHandler } from "./update-record.command-handler" +import { UpdateSpaceCommandHandler } from "./update-space.command-handler" import { UpdateTableFieldCommandHandler } from "./update-table-field.command-handler" import { UpdateTableCommandHandler } from "./update-table.command-handler" import { UpdateViewCommandHandler } from "./update-view.command-handler" @@ -82,4 +83,5 @@ export const commandHandlers = [ DeleteTableCommandHandler, CreateApiTokenCommandHandler, CreateSpaceCommandHandler, + UpdateSpaceCommandHandler, ] diff --git a/packages/command-handlers/src/handlers/update-space.command-handler.ts b/packages/command-handlers/src/handlers/update-space.command-handler.ts new file mode 100644 index 000000000..cf5b4c128 --- /dev/null +++ b/packages/command-handlers/src/handlers/update-space.command-handler.ts @@ -0,0 +1,28 @@ +import { UpdateSpaceCommand } from "@undb/commands" +import { mustGetCurrentSpaceId } from "@undb/context/server" +import { commandHandler } from "@undb/cqrs" +import { singleton } from "@undb/di" +import { type ICommandHandler } from "@undb/domain" +import { injectSpaceRepository, type ISpaceRepository } from "@undb/space" + +@commandHandler(UpdateSpaceCommand) +@singleton() +export class UpdateSpaceCommandHandler implements ICommandHandler { + constructor( + @injectSpaceRepository() + private readonly repository: ISpaceRepository, + ) {} + + async execute(command: UpdateSpaceCommand): Promise { + const spaceId = mustGetCurrentSpaceId() + const space = (await this.repository.findOneById(spaceId)).expect("Space not found") + + const spec = space.$update(command) + + if (spec.isSome()) { + await this.repository.updateOneById(space, spec.unwrap()) + } + + return space.id.value + } +} diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index a774d687b..9e4b3afdd 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -35,6 +35,7 @@ export * from "./set-view-sort.command" export * from "./update-account.command" export * from "./update-base.command" export * from "./update-record.command" +export * from "./update-space.command" export * from "./update-table-field.command" export * from "./update-table.command" export * from "./update-view.command" diff --git a/packages/commands/src/update-space.command.ts b/packages/commands/src/update-space.command.ts new file mode 100644 index 000000000..649a3eff6 --- /dev/null +++ b/packages/commands/src/update-space.command.ts @@ -0,0 +1,16 @@ +import { Command, type CommandProps } from "@undb/domain" +import { updateSpaceDTO } from "@undb/space" +import { z } from "@undb/zod" + +export const updateSpaceCommand = updateSpaceDTO + +export type IUpdateSpaceCommand = z.infer + +export class UpdateSpaceCommand extends Command implements IUpdateSpaceCommand { + public readonly name: string + + constructor(props: CommandProps) { + super(props) + this.name = props.name + } +} diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 48c12e888..596fb5b99 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -213,6 +213,7 @@ export class Graphql { id: ID! name: String! isPersonal: Boolean! + avatar: String member: SpaceMember } @@ -293,6 +294,7 @@ export class Graphql { } const space = (await this.queryBus.execute(new GetSpaceByIdQuery({ id: spaceId }))) as Option + if (space.isNone()) { return null } diff --git a/packages/persistence/src/space/space.mutate-visitor.ts b/packages/persistence/src/space/space.mutate-visitor.ts new file mode 100644 index 000000000..e7dbddd67 --- /dev/null +++ b/packages/persistence/src/space/space.mutate-visitor.ts @@ -0,0 +1,39 @@ +import type { + ISpaceSpecVisitor, + WithSpaceApiToken, + WithSpaceAvatar, + WithSpaceBaseId, + WithSpaceId, + WithSpaceIsPersonal, + WithSpaceName, + WithSpaceShareId, + WithSpaceUserId, +} from "@undb/space" +import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" + +export class SpaceMutateVisitor extends AbstractQBMutationVisitor implements ISpaceSpecVisitor { + withId(v: WithSpaceId): void { + throw new Error("Method not implemented.") + } + withUserId(v: WithSpaceUserId): void { + throw new Error("Method not implemented.") + } + withBaseId(v: WithSpaceBaseId): void { + throw new Error("Method not implemented.") + } + withShareId(v: WithSpaceShareId): void { + throw new Error("Method not implemented.") + } + withApiToken(v: WithSpaceApiToken): void { + throw new Error("Method not implemented.") + } + withIsPersonal(v: WithSpaceIsPersonal): void { + throw new Error("Method not implemented.") + } + withName(v: WithSpaceName): void { + this.setData("name", v.name.value) + } + withAvatar(v: WithSpaceAvatar): void { + this.setData("avatar", v.avatar?.value ?? null) + } +} diff --git a/packages/persistence/src/space/space.query-repository.ts b/packages/persistence/src/space/space.query-repository.ts index 4a555a6c7..d36168054 100644 --- a/packages/persistence/src/space/space.query-repository.ts +++ b/packages/persistence/src/space/space.query-repository.ts @@ -27,6 +27,7 @@ export class SpaceQueryRepository implements ISpaceQueryRepository { id: space.id, name: space.name ?? "", isPersonal: Boolean(space.is_personal), + avatar: space.avatar, })) } async findOneById(id: string): Promise> { @@ -38,6 +39,7 @@ export class SpaceQueryRepository implements ISpaceQueryRepository { return Some({ id: space.id, + avatar: space.avatar, name: space.name ?? "", isPersonal: Boolean(space.is_personal), }) diff --git a/packages/persistence/src/space/space.repository.ts b/packages/persistence/src/space/space.repository.ts index 5602e4de4..e78208342 100644 --- a/packages/persistence/src/space/space.repository.ts +++ b/packages/persistence/src/space/space.repository.ts @@ -6,6 +6,7 @@ import { getCurrentTransaction } from "../ctx" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { SpaceFilterVisitor } from "./space.filter-visitor" +import { SpaceMutateVisitor } from "./space.mutate-visitor" @singleton() export class SpaceRepostitory implements ISpaceRepository { @@ -40,8 +41,25 @@ export class SpaceRepostitory implements ISpaceRepository { }), ) } - findOneById(id: string): Promise> { - throw new Error("Method not implemented.") + async findOneById(id: string): Promise> { + const space = await (getCurrentTransaction() ?? this.qb) + .selectFrom("undb_space") + .selectAll() + .where("undb_space.id", "=", id) + .executeTakeFirst() + + if (!space) { + return None + } + + return Some( + SpaceFactory.fromJSON({ + id: space.id, + avatar: space.avatar, + name: space.name ?? "", + isPersonal: Boolean(space.is_personal), + }), + ) } async insert(space: Space): Promise { const tx = getCurrentTransaction() @@ -60,8 +78,16 @@ export class SpaceRepostitory implements ISpaceRepository { }) .execute() } - updateOneById(space: Space, spec: ISpaceSpecification): Promise { - throw new Error("Method not implemented.") + async updateOneById(space: Space, spec: ISpaceSpecification): Promise { + const visitor = new SpaceMutateVisitor() + spec.accept(visitor) + + const userId = getCurrentUserId() + await getCurrentTransaction() + .updateTable("undb_space") + .set({ ...visitor.data, updated_by: userId, updated_at: new Date().toISOString() }) + .where((eb) => eb.eb("id", "=", space.id.value)) + .execute() } deleteOneById(id: string): Promise { throw new Error("Method not implemented.") diff --git a/packages/space/src/dto/update-space.dto.ts b/packages/space/src/dto/update-space.dto.ts index 0f8fc6b19..0182368ce 100644 --- a/packages/space/src/dto/update-space.dto.ts +++ b/packages/space/src/dto/update-space.dto.ts @@ -1,8 +1,7 @@ import { z } from "@undb/zod" -import { spaceAvatarSchema, spaceIdSchema, spaceNameSchema } from "../value-objects" +import { spaceAvatarSchema, spaceNameSchema } from "../value-objects" export const updateSpaceDTO = z.object({ - id: spaceIdSchema, name: spaceNameSchema, avatar: spaceAvatarSchema.optional(), }) diff --git a/packages/space/src/space.service.ts b/packages/space/src/space.service.ts index 11640a69c..17ab304f1 100644 --- a/packages/space/src/space.service.ts +++ b/packages/space/src/space.service.ts @@ -24,7 +24,7 @@ interface IGetSpaceInput { } export interface ISpaceService { - createPersonalSpace(): Promise + createPersonalSpace(username: string): Promise getSpace(input: IGetSpaceInput): Promise> getMemberSpaces(userId: string): Promise setSpaceContext(setContext: SetContextValue, input: IGetSpaceInput): Promise @@ -43,9 +43,9 @@ export class SpaceService implements ISpaceService { private readonly spaceQueryRepository: ISpaceQueryRepository, ) {} - async createPersonalSpace(): Promise { + async createPersonalSpace(username: string): Promise { const space = SpaceFactory.create({ - name: "", + name: username + "'s Personal Space", isPersonal: true, }) diff --git a/packages/trpc/src/router.ts b/packages/trpc/src/router.ts index efae5462b..f9b7041c9 100644 --- a/packages/trpc/src/router.ts +++ b/packages/trpc/src/router.ts @@ -34,6 +34,7 @@ import { UpdateAccountCommand, UpdateBaseCommand, UpdateRecordCommand, + UpdateSpaceCommand, UpdateTableCommand, UpdateTableFieldCommand, UpdateViewCommand, @@ -73,6 +74,7 @@ import { setViewSortCommand, updateBaseCommand, updateRecordCommand, + updateSpaceCommand, updateTableCommand, updateTableFieldCommand, updateViewCommand, @@ -297,6 +299,7 @@ const apiTokenRouter = t.router({ const spaceRouter = t.router({ list: p.input(getMemberSpacesQuery).query(({ input }) => queryBus.execute(new GetMemberSpacesQuery(input))), create: p.input(createSpaceCommand).mutation(({ input }) => commandBus.execute(new CreateSpaceCommand(input))), + update: p.input(updateSpaceCommand).mutation(({ input }) => commandBus.execute(new UpdateSpaceCommand(input))), }) export const route = t.router({