mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 03:37:51 +01:00
Use tanstackdb for channel creation
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
|
||||
import { MutationHandler } from '@colanode/client/lib/types';
|
||||
import { MutationError, MutationErrorCode } from '@colanode/client/mutations';
|
||||
import {
|
||||
ChannelCreateMutationInput,
|
||||
ChannelCreateMutationOutput,
|
||||
} from '@colanode/client/mutations/channels/channel-create';
|
||||
import { ChannelAttributes, generateId, IdType } from '@colanode/core';
|
||||
|
||||
export class ChannelCreateMutationHandler
|
||||
extends WorkspaceMutationHandlerBase
|
||||
implements MutationHandler<ChannelCreateMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: ChannelCreateMutationInput
|
||||
): Promise<ChannelCreateMutationOutput> {
|
||||
const workspace = this.getWorkspace(input.userId);
|
||||
|
||||
const space = await workspace.database
|
||||
.selectFrom('nodes')
|
||||
.selectAll()
|
||||
.where('id', '=', input.spaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!space) {
|
||||
throw new MutationError(
|
||||
MutationErrorCode.SpaceNotFound,
|
||||
'Space not found or has been deleted.'
|
||||
);
|
||||
}
|
||||
|
||||
const id = generateId(IdType.Channel);
|
||||
const attributes: ChannelAttributes = {
|
||||
type: 'channel',
|
||||
name: input.name,
|
||||
avatar: input.avatar,
|
||||
parentId: input.spaceId,
|
||||
};
|
||||
|
||||
await workspace.nodes.createNode({
|
||||
id,
|
||||
attributes,
|
||||
parentId: input.spaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import { EmailRegisterMutationHandler } from './auth/email-register';
|
||||
import { EmailVerifyMutationHandler } from './auth/email-verify';
|
||||
import { GoogleLoginMutationHandler } from './auth/google-login';
|
||||
import { AvatarUploadMutationHandler } from './avatars/avatar-upload';
|
||||
import { ChannelCreateMutationHandler } from './channels/channel-create';
|
||||
import { ChannelUpdateMutationHandler } from './channels/channel-update';
|
||||
import { ChatCreateMutationHandler } from './chats/chat-create';
|
||||
import { DatabaseCreateMutationHandler } from './databases/database-create';
|
||||
@@ -42,6 +41,7 @@ import { MessageCreateMutationHandler } from './messages/message-create';
|
||||
import { NodeCollaboratorCreateMutationHandler } from './nodes/node-collaborator-create';
|
||||
import { NodeCollaboratorDeleteMutationHandler } from './nodes/node-collaborator-delete';
|
||||
import { NodeCollaboratorUpdateMutationHandler } from './nodes/node-collaborator-update';
|
||||
import { NodeCreateMutationHandler } from './nodes/node-create';
|
||||
import { NodeDeleteMutationHandler } from './nodes/node-delete';
|
||||
import { NodeInteractionOpenedMutationHandler } from './nodes/node-interaction-opened';
|
||||
import { NodeInteractionSeenMutationHandler } from './nodes/node-interaction-seen';
|
||||
@@ -80,8 +80,8 @@ export const buildMutationHandlerMap = (
|
||||
'email.verify': new EmailVerifyMutationHandler(app),
|
||||
'google.login': new GoogleLoginMutationHandler(app),
|
||||
'view.create': new ViewCreateMutationHandler(app),
|
||||
'channel.create': new ChannelCreateMutationHandler(app),
|
||||
'node.delete': new NodeDeleteMutationHandler(app),
|
||||
'node.create': new NodeCreateMutationHandler(app),
|
||||
'chat.create': new ChatCreateMutationHandler(app),
|
||||
'database.create': new DatabaseCreateMutationHandler(app),
|
||||
'database.name.field.update': new DatabaseNameFieldUpdateMutationHandler(
|
||||
|
||||
22
packages/client/src/handlers/mutations/nodes/node-create.ts
Normal file
22
packages/client/src/handlers/mutations/nodes/node-create.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
|
||||
import { MutationHandler } from '@colanode/client/lib/types';
|
||||
import {
|
||||
NodeCreateMutationInput,
|
||||
NodeCreateMutationOutput,
|
||||
} from '@colanode/client/mutations/nodes/node-create';
|
||||
|
||||
export class NodeCreateMutationHandler
|
||||
extends WorkspaceMutationHandlerBase
|
||||
implements MutationHandler<NodeCreateMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: NodeCreateMutationInput
|
||||
): Promise<NodeCreateMutationOutput> {
|
||||
const workspace = this.getWorkspace(input.userId);
|
||||
await workspace.nodes.insertNode(input.node);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
export type ChannelCreateMutationInput = {
|
||||
type: 'channel.create';
|
||||
userId: string;
|
||||
spaceId: string;
|
||||
name: string;
|
||||
avatar?: string | null;
|
||||
};
|
||||
|
||||
export type ChannelCreateMutationOutput = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
declare module '@colanode/client/mutations' {
|
||||
interface MutationMap {
|
||||
'channel.create': {
|
||||
input: ChannelCreateMutationInput;
|
||||
output: ChannelCreateMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ export * from './auth/google-login';
|
||||
export * from './apps/metadata-delete';
|
||||
export * from './apps/metadata-update';
|
||||
export * from './avatars/avatar-upload';
|
||||
export * from './channels/channel-create';
|
||||
export * from './channels/channel-update';
|
||||
export * from './chats/chat-create';
|
||||
export * from './databases/database-create';
|
||||
@@ -62,6 +61,7 @@ export * from './apps/tab-update';
|
||||
export * from './servers/server-sync';
|
||||
export * from './apps/tab-delete';
|
||||
export * from './nodes/node-delete';
|
||||
export * from './nodes/node-create';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface MutationMap {}
|
||||
|
||||
20
packages/client/src/mutations/nodes/node-create.ts
Normal file
20
packages/client/src/mutations/nodes/node-create.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { LocalNode } from '@colanode/client/types';
|
||||
|
||||
export type NodeCreateMutationInput = {
|
||||
type: 'node.create';
|
||||
userId: string;
|
||||
node: LocalNode;
|
||||
};
|
||||
|
||||
export type NodeCreateMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@colanode/client/mutations' {
|
||||
interface MutationMap {
|
||||
'node.create': {
|
||||
input: NodeCreateMutationInput;
|
||||
output: NodeCreateMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@colanode/client/lib/mentions';
|
||||
import { deleteNodeRelations, fetchNodeTree } from '@colanode/client/lib/utils';
|
||||
import { WorkspaceService } from '@colanode/client/services/workspaces/workspace-service';
|
||||
import { DownloadStatus } from '@colanode/client/types';
|
||||
import { DownloadStatus, LocalNode } from '@colanode/client/types';
|
||||
import {
|
||||
generateId,
|
||||
IdType,
|
||||
@@ -225,6 +225,173 @@ export class NodeService {
|
||||
return createdNode;
|
||||
}
|
||||
|
||||
public async insertNode(input: LocalNode): Promise<LocalNode> {
|
||||
debug(`Inserting node ${input.id} with type ${input.attributes.type}`);
|
||||
|
||||
const tree = input.parentId
|
||||
? await fetchNodeTree(this.workspace.database, input.parentId)
|
||||
: [];
|
||||
|
||||
const model = getNodeModel(input.attributes.type);
|
||||
const canCreateNodeContext: CanCreateNodeContext = {
|
||||
user: {
|
||||
id: this.workspace.userId,
|
||||
role: this.workspace.role,
|
||||
workspaceId: this.workspace.workspaceId,
|
||||
accountId: this.workspace.accountId,
|
||||
},
|
||||
tree: tree,
|
||||
attributes: input.attributes,
|
||||
};
|
||||
|
||||
if (!model.canCreate(canCreateNodeContext)) {
|
||||
throw new Error('Insufficient permissions');
|
||||
}
|
||||
|
||||
const ydoc = new YDoc();
|
||||
const update = ydoc.update(model.attributesSchema, input.attributes);
|
||||
|
||||
if (!update) {
|
||||
throw new Error('Invalid attributes');
|
||||
}
|
||||
|
||||
const updateId = generateId(IdType.Update);
|
||||
const createdAt = new Date().toISOString();
|
||||
const rootId = tree[0]?.id ?? input.id;
|
||||
const nodeText = model.extractText(input.id, input.attributes);
|
||||
const mentions = model.extractMentions(input.id, input.attributes);
|
||||
const nodeReferencesToCreate: CreateNodeReference[] = mentions.map(
|
||||
(mention) => ({
|
||||
node_id: input.id,
|
||||
reference_id: mention.target,
|
||||
inner_id: mention.id,
|
||||
type: 'mention',
|
||||
created_at: createdAt,
|
||||
created_by: this.workspace.userId,
|
||||
})
|
||||
);
|
||||
|
||||
const { createdNode, createdMutation, createdNodeReferences } =
|
||||
await this.workspace.database.transaction().execute(async (trx) => {
|
||||
const createdNode = await trx
|
||||
.insertInto('nodes')
|
||||
.returningAll()
|
||||
.values({
|
||||
id: input.id,
|
||||
root_id: rootId,
|
||||
attributes: JSON.stringify(input.attributes),
|
||||
created_at: createdAt,
|
||||
created_by: this.workspace.userId,
|
||||
local_revision: '0',
|
||||
server_revision: '0',
|
||||
})
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!createdNode) {
|
||||
throw new Error('Failed to create node');
|
||||
}
|
||||
|
||||
const createdNodeUpdate = await trx
|
||||
.insertInto('node_updates')
|
||||
.returningAll()
|
||||
.values({
|
||||
id: updateId,
|
||||
node_id: input.id,
|
||||
data: update,
|
||||
created_at: createdAt,
|
||||
})
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!createdNodeUpdate) {
|
||||
throw new Error('Failed to create node update');
|
||||
}
|
||||
|
||||
const mutationData: CreateNodeMutationData = {
|
||||
nodeId: input.id,
|
||||
updateId: updateId,
|
||||
data: encodeState(update),
|
||||
createdAt: createdAt,
|
||||
};
|
||||
|
||||
const createdMutation = await trx
|
||||
.insertInto('mutations')
|
||||
.returningAll()
|
||||
.values({
|
||||
id: generateId(IdType.Mutation),
|
||||
type: 'node.create',
|
||||
data: JSON.stringify(mutationData),
|
||||
created_at: createdAt,
|
||||
retries: 0,
|
||||
})
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!createdMutation) {
|
||||
throw new Error('Failed to create mutation');
|
||||
}
|
||||
|
||||
if (nodeText) {
|
||||
await trx
|
||||
.insertInto('node_texts')
|
||||
.values({
|
||||
id: input.id,
|
||||
name: nodeText.name,
|
||||
attributes: nodeText.attributes,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
let createdNodeReferences: SelectNodeReference[] = [];
|
||||
if (nodeReferencesToCreate.length > 0) {
|
||||
createdNodeReferences = await trx
|
||||
.insertInto('node_references')
|
||||
.values(nodeReferencesToCreate)
|
||||
.returningAll()
|
||||
.execute();
|
||||
}
|
||||
|
||||
return {
|
||||
createdNode,
|
||||
createdMutation,
|
||||
createdNodeReferences,
|
||||
};
|
||||
});
|
||||
|
||||
if (!createdNode) {
|
||||
throw new Error('Failed to create node');
|
||||
}
|
||||
|
||||
if (!createdMutation) {
|
||||
throw new Error('Failed to create mutation');
|
||||
}
|
||||
|
||||
debug(`Created node ${createdNode.id} with type ${createdNode.type}`);
|
||||
|
||||
eventBus.publish({
|
||||
type: 'node.created',
|
||||
workspace: {
|
||||
workspaceId: this.workspace.workspaceId,
|
||||
userId: this.workspace.userId,
|
||||
accountId: this.workspace.accountId,
|
||||
},
|
||||
node: mapNode(createdNode),
|
||||
});
|
||||
|
||||
for (const createdNodeReference of createdNodeReferences) {
|
||||
eventBus.publish({
|
||||
type: 'node.reference.created',
|
||||
workspace: {
|
||||
workspaceId: this.workspace.workspaceId,
|
||||
userId: this.workspace.userId,
|
||||
accountId: this.workspace.accountId,
|
||||
},
|
||||
nodeReference: mapNodeReference(createdNodeReference),
|
||||
});
|
||||
}
|
||||
|
||||
this.workspace.mutations.scheduleSync();
|
||||
return mapNode(createdNode);
|
||||
}
|
||||
|
||||
public async updateNode<T extends NodeAttributes>(
|
||||
nodeId: string,
|
||||
updater: (attributes: T) => T
|
||||
|
||||
@@ -60,15 +60,15 @@ export const createNodesCollection = (userId: string) => {
|
||||
};
|
||||
},
|
||||
},
|
||||
// onInsert: async ({ transaction }) => {
|
||||
// for (const mutation of transaction.mutations) {
|
||||
// await window.colanode.executeMutation({
|
||||
// type: 'node.create',
|
||||
// userId,
|
||||
// node: mutation.modified,
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
onInsert: async ({ transaction }) => {
|
||||
for (const mutation of transaction.mutations) {
|
||||
await window.colanode.executeMutation({
|
||||
type: 'node.create',
|
||||
userId,
|
||||
node: mutation.modified,
|
||||
});
|
||||
}
|
||||
},
|
||||
// onUpdate: async ({ transaction }) => {
|
||||
// for (const mutation of transaction.mutations) {
|
||||
// const attributes = cloneDeep(mutation.modified.attributes);
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { LocalChannelNode } from '@colanode/client/types';
|
||||
import { generateId, IdType } from '@colanode/core';
|
||||
import { ChannelForm } from '@colanode/ui/components/channels/channel-form';
|
||||
import { collections } from '@colanode/ui/collections';
|
||||
import {
|
||||
ChannelForm,
|
||||
ChannelFormValues,
|
||||
} from '@colanode/ui/components/channels/channel-form';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -11,7 +17,6 @@ import {
|
||||
DialogTitle,
|
||||
} from '@colanode/ui/components/ui/dialog';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
|
||||
interface ChannelCreateDialogProps {
|
||||
spaceId: string;
|
||||
@@ -26,7 +31,46 @@ export const ChannelCreateDialog = ({
|
||||
}: ChannelCreateDialogProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const navigate = useNavigate({ from: '/workspace/$userId' });
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: async (values: ChannelFormValues) => {
|
||||
const channelId = generateId(IdType.Channel);
|
||||
const nodes = collections.workspace(workspace.userId).nodes;
|
||||
|
||||
const channel: LocalChannelNode = {
|
||||
id: channelId,
|
||||
type: 'channel',
|
||||
attributes: {
|
||||
type: 'channel',
|
||||
name: values.name,
|
||||
parentId: spaceId,
|
||||
},
|
||||
parentId: spaceId,
|
||||
rootId: spaceId,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: workspace.userId,
|
||||
updatedAt: null,
|
||||
updatedBy: null,
|
||||
localRevision: '0',
|
||||
serverRevision: '0',
|
||||
};
|
||||
|
||||
nodes.insert(channel);
|
||||
return channel;
|
||||
},
|
||||
onSuccess: (channel) => {
|
||||
navigate({
|
||||
to: '$nodeId',
|
||||
params: {
|
||||
nodeId: channel.id,
|
||||
},
|
||||
});
|
||||
onOpenChange(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -42,38 +86,11 @@ export const ChannelCreateDialog = ({
|
||||
values={{
|
||||
name: '',
|
||||
}}
|
||||
isPending={isPending}
|
||||
submitText="Create"
|
||||
handleCancel={() => {
|
||||
onCancel={() => {
|
||||
onOpenChange(false);
|
||||
}}
|
||||
handleSubmit={(values) => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutate({
|
||||
input: {
|
||||
type: 'channel.create',
|
||||
spaceId: spaceId,
|
||||
name: values.name,
|
||||
avatar: values.avatar,
|
||||
userId: workspace.userId,
|
||||
},
|
||||
onSuccess(output) {
|
||||
onOpenChange(false);
|
||||
navigate({
|
||||
to: '$nodeId',
|
||||
params: {
|
||||
nodeId: output.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
onSubmit={(values) => mutate(values)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -14,30 +14,29 @@ import {
|
||||
FormMessage,
|
||||
} from '@colanode/ui/components/ui/form';
|
||||
import { Input } from '@colanode/ui/components/ui/input';
|
||||
import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(3, 'Name must be at least 3 characters long.'),
|
||||
avatar: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type ChannelFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface ChannelFormProps {
|
||||
id: string;
|
||||
values: z.infer<typeof formSchema>;
|
||||
isPending: boolean;
|
||||
submitText: string;
|
||||
handleCancel: () => void;
|
||||
handleSubmit: (values: z.infer<typeof formSchema>) => void;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: z.infer<typeof formSchema>) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const ChannelForm = ({
|
||||
id,
|
||||
values,
|
||||
isPending,
|
||||
submitText,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
readOnly = false,
|
||||
}: ChannelFormProps) => {
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -60,10 +59,7 @@ export const ChannelForm = ({
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col"
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<form className="flex flex-col" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="grow flex flex-row items-end gap-2 py-2 pb-4">
|
||||
{readOnly ? (
|
||||
<Button type="button" variant="outline" size="icon">
|
||||
@@ -72,7 +68,6 @@ export const ChannelForm = ({
|
||||
) : (
|
||||
<AvatarPopover
|
||||
onPick={(avatar) => {
|
||||
if (isPending) return;
|
||||
if (avatar === values.avatar) return;
|
||||
|
||||
form.setValue('avatar', avatar);
|
||||
@@ -102,16 +97,10 @@ export const ChannelForm = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isPending}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending || readOnly}>
|
||||
{isPending && <Spinner className="mr-1" />}
|
||||
<Button type="submit" disabled={readOnly}>
|
||||
{submitText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -45,13 +45,12 @@ export const ChannelUpdateDialog = ({
|
||||
name: channel.attributes.name,
|
||||
avatar: channel.attributes.avatar,
|
||||
}}
|
||||
isPending={isPending}
|
||||
submitText="Update"
|
||||
readOnly={!canEdit}
|
||||
handleCancel={() => {
|
||||
onCancel={() => {
|
||||
onOpenChange(false);
|
||||
}}
|
||||
handleSubmit={(values) => {
|
||||
onSubmit={(values) => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user