1
9import type { Position } from "@factory/shared/positions";
10
11
12
13
14export type { Position };
15import type { BlockChangedFrame } from "@factory/shared/realtime/frames";
16
17
18
19export interface User {
20 id: string;
21 name: string;
22 email: string;
23 image?: string | null;
24 createdAt: string;
25}
26
27
28
29export type WorkspaceRole = "owner" | "admin" | "member" | "guest";
30
31export interface Workspace {
32 id: string;
33 name: string;
34 slug: string;
35 emoji?: string | null;
36 plan: "free" | "pro" | "enterprise";
37 role: WorkspaceRole;
38 memberCount: number;
39 createdAt: string;
40
41 type?: string;
42
43 archivedAt?: string | null;
44}
45
46
49export interface ActivityRow {
50 id: string;
51 actorType: string;
52 actorId: string;
53 action: string;
54 resourceType: string;
55 resourceId: string;
56 workspaceId: string;
57 pageId: string | null;
58 createdAt: string;
59 actorName?: string | null;
60}
61
62
63export interface WorkspaceStats {
64 workspaceId: string;
65 type: string;
66 archivedAt: string | null;
67 content: {
68 pages: number;
69 whiteboards: number;
70 tasks: number;
71 databases: number;
72 records: number;
73 comments: number;
74 members: number;
75 storageBytes: number;
76 };
77}
78
79export interface WorkspaceMember {
80 userId: string;
81 user: Pick<User, "id" | "name" | "email" | "image">;
82 role: WorkspaceRole;
83 joinedAt: string;
84}
85
86
87export interface Team {
88 id: string;
89 name: string;
90 slug: string;
91 memberCount: number;
92}
93
94export interface WorkspaceInvite {
95 id: string;
96 workspaceId: string;
97 email: string;
98 role: WorkspaceRole;
99 token: string;
100 expiresAt: string;
101 acceptedAt: string | null;
102 createdAt: string;
103}
104
105
106
107
108export type PageType = "page" | "database" | "trash";
109
110export interface Page {
111 id: string;
112 workspaceId: string;
113
114 parentId: string | null;
115 title: string;
116 icon?: string | null;
117 cover?: string | null;
118 type: PageType;
119
120 position: Position;
121 favorite: boolean;
122 createdBy: string;
123 createdByUser?: Pick<User, "id" | "name" | "image"> | null;
124 updatedAt: string;
125 createdAt: string;
126 trashedAt: string | null;
127
133 databaseId?: string | null;
134
140 kind?: "page" | "whiteboard" | "task";
141}
142
143
149export interface Task {
150 id: string;
151 workspaceId: string;
152 pageId: string;
153 title: string;
154 status: "todo" | "in_progress" | "done" | "cancelled";
155 dueAt: string | null;
156 assigneeIds: string[];
157 createdBy: string;
158 createdAt: string;
159 updatedAt: string;
160
161 page?: { id: string; title: string; trashedAt: string | null; reviewState: string; parentId: string | null };
162 pageTitle?: string;
163}
164
165
174export interface PageReview {
175 state: "draft" | "in_review" | "approved" | "published" | "changes_requested";
176 review: {
177 id: string;
178 state: string;
179 reviewerIds: string[];
180 requestedBy: string;
181 comment: string | null;
182 deadline: string | null;
183 openedAt: string;
184 } | null;
185 lastReview: { id: string; reviewerIds: string[]; decision: string | null; comment: string | null } | null;
186}
187
188export interface PageTreeEntry {
189 id: string;
190 parentId: string | null;
191 title: string;
192 icon?: string | null;
193 type: PageType;
194 position: Position;
195
196 children?: PageTreeEntry[];
197
198 hasMore?: boolean;
199 updatedAt: string;
200}
201
202
208export interface TemplateInfo {
209 id: string;
210 name: string;
211 icon: string | null;
212 type: string;
213 isBuiltin: boolean;
214
215 sourceRef: unknown;
216 createdAt: string;
217
218 category?: string | null;
219
220 description?: string | null;
221
222 variables?: unknown;
223
224 defaultFor?: string | null;
225}
226
227
228export interface TemplateVersionInfo {
229 id: string;
230 version: number;
231 note: string | null;
232 createdBy: string;
233 createdAt: string;
234}
235
236
243export interface PageVersion {
244 id: string;
245
246 version: number;
247
248 note: string | null;
249 createdBy: string;
250 createdAt: string;
251}
252
253
254export interface VersionPreview {
255 version: number;
256 createdAt: string;
257 note: string | null;
258 snapshot: {
259 page: Record<string, unknown>;
260 blocks: Array<{
261 id: string;
262 type: string;
263 content: unknown;
264 properties: Record<string, unknown> | null;
265 parentId: string | null;
266 position: string;
267 }>;
268 };
269}
270
271
277export interface VersionDiff {
278 from: number;
279 to: number;
280 page: Record<string, { from: unknown; to: unknown }>;
281 added: Array<{ id: string; type: string; content: unknown }>;
282 deleted: Array<{ id: string; type: string; content: unknown }>;
283 modified: Array<{ id: string; fields: string[] }>;
284 moved: Array<{ id: string; from: string; to: string }>;
285 summary: {
286 added: number;
287 deleted: number;
288 modified: number;
289 moved: number;
290 unchanged: number;
291 };
292}
293
294export interface PageShare {
295 id: string;
296 pageId: string;
297 slug: string;
298 url: string;
299 level: "view" | "edit" | "public";
300 password?: string | null;
301 expiresAt: string | null;
302 createdAt: string;
303 updatedAt: string;
304}
305
306
307
308
313export const BLOCK_TYPES = [
314 "paragraph",
315 "heading_1",
316 "heading_2",
317 "heading_3",
318 "quote",
319 "callout",
320 "bulleted_list",
321 "numbered_list",
322 "todo",
323 "toggle",
324 "image",
325 "video",
326 "audio",
327 "file",
328 "bookmark",
329 "embed",
330 "code",
331 "equation",
332 "column",
333 "column_list",
334 "divider",
335 "spacer",
336 "database",
337 "linked_database",
338 "table",
339 "chart",
340 "form",
341 "synced_block",
342 "child_page",
343 "child_database",
344 "toc",
345] as const;
346export type BlockType = (typeof BLOCK_TYPES)[number];
347
348export interface BlockTableColumn {
349 id: string;
350 type: "text" | "number";
351 width: number;
352
353 label?: string;
354}
355
356export interface BlockProperties {
357
359 isTitle?: boolean;
360
361 align?: string;
362
363 language?: string;
364
365 code?: string;
366
367 showLineNumbers?: boolean;
368 wrap?: boolean;
369
370 fileId?: string;
371
372 src?: string;
373 url?: string;
374 caption?: string;
375 name?: string;
376 size?: number;
377 mimeType?: string;
378 title?: string;
379 description?: string;
380
381 citation?: string;
382
383 emoji?: string;
384 backgroundColor?: string;
385 textColor?: string;
386 icon?: string;
387 color?: string;
388
389 variant?: string;
390
391 checked?: boolean;
392
393 collapsed?: boolean;
394
395 start?: number;
396
397 columns?: BlockTableColumn[];
398 rows?: string[][];
399 headerRow?: boolean;
400 columnWidths?: number[];
401
402 widthPct?: number;
403
406 widthPercent?: number;
407
408 display?: "fullPage" | "pageWidth" | "auto";
409 heightPx?: number;
410
411 latex?: string;
412
413 background?: string;
414 padding?: string;
415
416 sourceSynced?: boolean;
417 targetBlockId?: string;
418 targetPageId?: string;
419
420 pageId?: string;
421
422 databaseId?: string;
423 viewId?: string;
424
425 chartId?: string;
426 formId?: string;
427}
428
429export interface Block {
430 id: string;
431 pageId: string;
432
433 parentId: string | null;
434 type: BlockType;
435 position: Position;
436
437 content: unknown;
438 properties: BlockProperties;
439 updatedAt: string;
440 createdAt: string;
441}
442
443export type BlockPatch = Partial<
444 Pick<Block, "type" | "content" | "properties" | "parentId" | "position">
445>;
446
447
448
449export const PROPERTY_TYPES = [
450 "title",
451 "text",
452 "rich_text",
453 "number",
454 "select",
455 "multi_select",
456 "status",
457 "date",
458 "date_range",
459 "person",
460 "email",
461 "phone",
462 "url",
463 "checkbox",
464 "files",
465 "created_time",
466 "created_by",
467 "last_edited_time",
468 "last_edited_by",
469 "relation",
470 "rollup",
471 "formula",
472 "button",
473 "unique_id",
474] as const;
475export type PropertyType = (typeof PROPERTY_TYPES)[number];
476
477export const VIEW_TYPES = [
478 "table",
479 "board",
480 "calendar",
481 "timeline",
482 "list",
483 "gallery",
484 "chart",
485 "form",
486 "dashboard",
487] as const;
488export type ViewType = (typeof VIEW_TYPES)[number];
489
490export interface DatabaseProperty {
491 id: string;
492 databaseId: string;
493 name: string;
494 type: PropertyType;
495 position: Position;
496
497 config?: Record<string, unknown>;
498}
499
500export interface DatabaseView {
501 id: string;
502 databaseId: string;
503 name: string;
504 type: ViewType;
505 position: Position;
506
507 config?: Record<string, unknown>;
508}
509
510export interface Database {
511 id: string;
512 workspaceId: string;
513 pageId: string;
514 title: string;
515 properties: DatabaseProperty[];
516 views: DatabaseView[];
517 icon?: string | null;
518 createdAt: string;
519 updatedAt: string;
520}
521
522export type RecordValue = string | number | boolean | string[] | null;
523
524export interface DatabaseRecord {
525 id: string;
526 databaseId: string;
527 pageId: string | null;
528 values: Record<string, RecordValue>;
529 createdAt: string;
530 updatedAt: string;
531}
532
533
534
535export interface Comment {
536 id: string;
537 blockId: string | null;
538 pageId: string;
539 parentId: string | null;
540 body: string;
541 authorId: string;
542 author?: Pick<User, "id" | "name" | "image"> | null;
543 resolved: boolean;
544 createdAt: string;
545 updatedAt: string;
546}
547
548export interface FileMeta {
549 id: string;
550 workspaceId: string;
551 name: string;
552 mime: string;
553 size: number;
554
560 url?: string;
561 status: "processing" | "ready" | "error";
562 createdAt: string;
563}
564
565
566
567
571export type SearchType =
572 | "page"
573 | "block"
574 | "database"
575 | "record"
576 | "comment"
577 | "file"
578 | "user";
579
580
586export interface SearchResult {
587 docId: string;
588 type: SearchType;
589 id: string;
590 title: string;
591 snippet?: string | null;
592 url?: string | null;
593 icon?: string | null;
594 createdByName?: string | null;
595 updatedAt: string;
596 pageId?: string | null;
597 blockId?: string | null;
598 databaseId?: string | null;
599 recordId?: string | null;
600 workspaceId: string;
601 score: number;
602 source: string;
603
608 path?: string[];
609}
610
611
612export interface SearchQueryResult {
613 data: SearchResult[];
614 nextCursor: string | null;
615 tookMs: number;
616}
617
618
619export interface SearchSuggestion {
620 id: string;
621 type: SearchType;
622 title: string;
623}
624
625
626
627export interface BacklinkRef {
628 pageId: string;
629 title: string;
630 icon: string | null;
631 kind: string;
632}
633
634export interface SearchQueryInput {
635 q: string;
636 types?: SearchType[];
637 workspaceId?: string;
638 teamId?: string;
639 createdBy?: string;
640 updatedAfter?: string;
641 updatedBefore?: string;
642
643 sortBy?: "relevance" | "createdAt" | "updatedAt";
644 cursor?: string;
645
646 limit?: number;
647}
648
649
655export interface NotificationLink {
656 type: "page" | "database" | "record" | "comment" | (string & {});
657 id: string;
658 pageId?: string | null;
659}
660
661
666export interface Notification {
667 id: string;
668 workspaceId: string;
669
670 type:
671 | "MENTION"
672 | "COMMENT"
673 | "ASSIGNMENT"
674 | "PAGE_SHARED"
675 | "TASK_ASSIGNED"
676 | "DATABASE_CHANGE"
677 | "AUTOMATION"
678 | "INTEGRATION";
679
681 channel: "IN_APP" | "EMAIL" | "WEBHOOK";
682 title: string;
683 body?: string | null;
684 link?: NotificationLink | null;
685
686 readAt: string | null;
687
688 actor?: { id: string; name: string; avatarUrl: string | null } | null;
689 createdAt: string;
690}
691
692
693export type UnreadNotification = Notification & { readAt: null };
694
695
696
697
705export interface Presence {
706 userId: string;
707 name: string;
708 color: string;
709
710 cursor?: { blockId: string; offset: number; selection?: PresenceSelection } | null;
711
713 viewing?: boolean;
714
715 lastSeen: number;
716}
717
718
719export interface PresenceSelection {
720 fromBlockId: string;
721 fromOffset: number;
722 toBlockId: string;
723 toOffset: number;
724}
725
726
731export interface OwnPresence {
732 cursor?: { blockId: string; offset: number; selection?: PresenceSelection } | null;
733 viewing?: boolean;
734}
735
736
742export type RealtimeServerEvent =
743 | {
744 type: "presence";
745 pageId: string;
746 user: { id: string; name: string; color: string };
747 cursor?: { blockId: string; offset: number; selection?: PresenceSelection } | null;
748 viewing?: boolean;
749 lastSeen: number;
750 }
751 | { type: "presence-leave"; pageId: string; userId: string }
752 | { type: "block:upsert"; pageId: string; blocks: Block[] }
753 | { type: "block:delete"; pageId: string; blockIds: string[] }
754 | {
755 type: "block:move";
756 pageId: string;
757 blockId: string;
758 parentId: string | null;
759 position: Position;
760 }
761
764 | BlockChangedFrame
765
771 | {
772 type: "comment.event";
773 kind: "created" | "replied" | "resolved" | "reopened" | "reacted" | "deleted";
774 commentId: string;
775 pageId: string;
776 actorId: string;
777 preview: string;
778 }
779
786 | {
787 type: "notification.push";
788 notificationId: string;
789 kind: string;
790 title: string;
791 deepLink: { type: string; id: string };
792 actorName?: string;
793 }
794 | { type: "ack"; requestId: string };
795