Skip to content

Commit 65c62a0

Browse files
authored
blocknote schema and import/export validation (#40)
* add import/export tests * fix tests and markdown handling * add strong validation of blocknote schema * update README * switch to strict zod objects
1 parent ece3d0d commit 65c62a0

25 files changed

Lines changed: 2086 additions & 133 deletions

File tree

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
![Yjs](https://img.shields.io/badge/Yjs-6EDB8F?style=for-the-badge)
1313
![Better Auth](https://img.shields.io/badge/Better_Auth-1A1A2E?style=for-the-badge)
1414
![Cloudflare Workers](https://img.shields.io/badge/Cloudflare_Workers-F38020?style=for-the-badge&logo=cloudflare&logoColor=white)
15+
![Zod](https://img.shields.io/badge/Zod-3E67B1?style=for-the-badge&logo=zod&logoColor=white)
1516
![Vite](https://img.shields.io/badge/Vite-646CFF?style=for-the-badge&logo=vite&logoColor=white)
1617

1718
A campaign management platform built for Dungeon Masters and their players. Organize your world, share what your players need to see, and keep your secrets safe — all in real time.
@@ -86,17 +87,18 @@ A campaign management platform built for Dungeon Masters and their players. Orga
8687
| **Canvases** | ReactFlow, Yjs |
8788
| **Maps** | React Zoom Pan Pinch |
8889
| **Drag & Drop** | Atlaskit Pragmatic Drag and Drop |
90+
| **Validation** | Zod, Convex Validators |
8991
| **Backend** | Convex (real-time database, serverless functions, file storage) |
9092
| **Auth** | Better Auth (email/password, OAuth, 2FA) |
9193
| **Deployment** | Cloudflare Workers (serverless edge) |
9294
| **Tooling** | Vite+, Vitest, Playwright, React Compiler |
9395

9496
## Testing
9597

96-
| Suite | Runner | Scope |
97-
| ------------ | --------------------- | ----------------------------------------------------------------------------------------------------------- |
98-
| **Backend** | Vitest (edge-runtime) | 53 test files, 651 tests — mutations, queries, permissions, cascading deletes, sharing workflows, snapshots |
99-
| **Frontend** | Vitest (jsdom) | 24 test files, 358 tests — components, hooks, stores, utilities |
100-
| **E2E** | Playwright (Chromium) | Full user flows — campaign creation, note editing, sharing, navigation |
98+
| Suite | Runner | Scope |
99+
| ------------ | --------------------- | --------------------------------------------------------------------------------------------- |
100+
| **Backend** | Vitest (edge-runtime) | 600+ tests — mutations, queries, permissions, cascading deletes, sharing workflows, snapshots |
101+
| **Frontend** | Vitest (jsdom) | 300+ tests — components, hooks, stores, utilities |
102+
| **E2E** | Playwright (Chromium) | Full user flows — campaign creation, note editing, sharing, navigation |
101103

102104
Backend and frontend suites run on every push via GitHub Actions. E2E tests run in CI and locally against a live app instance.

convex/_generated/api.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type * as blockShares_functions_unshareBlocks from "../blockShares/functi
2323
import type * as blockShares_mutations from "../blockShares/mutations.js";
2424
import type * as blockShares_queries from "../blockShares/queries.js";
2525
import type * as blockShares_types from "../blockShares/types.js";
26+
import type * as blocks_blockNoteValidator from "../blocks/blockNoteValidator.js";
2627
import type * as blocks_functions_findBlockByBlockNoteId from "../blocks/functions/findBlockByBlockNoteId.js";
2728
import type * as blocks_functions_getBlockWithShares from "../blocks/functions/getBlockWithShares.js";
2829
import type * as blocks_functions_getBlocksWithShares from "../blocks/functions/getBlocksWithShares.js";
@@ -115,6 +116,7 @@ import type * as gameMaps_triggers from "../gameMaps/triggers.js";
115116
import type * as gameMaps_types from "../gameMaps/types.js";
116117
import type * as gameMaps_validation from "../gameMaps/validation.js";
117118
import type * as http from "../http.js";
119+
import type * as notes_blocknote from "../notes/blocknote.js";
118120
import type * as notes_editorSpecs from "../notes/editorSpecs.js";
119121
import type * as notes_functions_captureNoteSnapshot from "../notes/functions/captureNoteSnapshot.js";
120122
import type * as notes_functions_createNote from "../notes/functions/createNote.js";
@@ -234,6 +236,7 @@ declare const fullApi: ApiFromModules<{
234236
"blockShares/mutations": typeof blockShares_mutations;
235237
"blockShares/queries": typeof blockShares_queries;
236238
"blockShares/types": typeof blockShares_types;
239+
"blocks/blockNoteValidator": typeof blocks_blockNoteValidator;
237240
"blocks/functions/findBlockByBlockNoteId": typeof blocks_functions_findBlockByBlockNoteId;
238241
"blocks/functions/getBlockWithShares": typeof blocks_functions_getBlockWithShares;
239242
"blocks/functions/getBlocksWithShares": typeof blocks_functions_getBlocksWithShares;
@@ -326,6 +329,7 @@ declare const fullApi: ApiFromModules<{
326329
"gameMaps/types": typeof gameMaps_types;
327330
"gameMaps/validation": typeof gameMaps_validation;
328331
http: typeof http;
332+
"notes/blocknote": typeof notes_blocknote;
329333
"notes/editorSpecs": typeof notes_editorSpecs;
330334
"notes/functions/captureNoteSnapshot": typeof notes_functions_captureNoteSnapshot;
331335
"notes/functions/createNote": typeof notes_functions_createNote;

convex/_test/factories.helper.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,24 @@ import type schema from '../schema'
88
import type { SidebarItemLocation, SidebarItemType } from '../sidebarItems/types/baseTypes'
99
import type { PermissionLevel } from '../permissions/types'
1010
import type { ShareStatus } from '../blockShares/types'
11+
import type { CustomBlock } from '../notes/editorSpecs'
1112

1213
type T = TestConvex<typeof schema>
1314

15+
/** Create a test block content object typed as CustomBlock */
16+
export function testBlock(
17+
id: string,
18+
overrides?: Partial<{ type: string; props: Record<string, unknown>; content: Array<unknown> }>,
19+
): CustomBlock {
20+
return {
21+
id,
22+
type: 'paragraph',
23+
props: {},
24+
content: [],
25+
...overrides,
26+
} as CustomBlock
27+
}
28+
1429
let counter = 0
1530

1631
function nextId() {
@@ -341,7 +356,7 @@ export async function createBlock(
341356
overrides?: Partial<{
342357
blockId: string
343358
position: number | null
344-
content: Record<string, unknown>
359+
content: CustomBlock
345360
isTopLevel: boolean
346361
shareStatus: ShareStatus | null
347362
deletionTime: number | null
@@ -354,7 +369,7 @@ export async function createBlock(
354369
noteId: noteId,
355370
blockId: `block-${n}`,
356371
position: null,
357-
content: { id: `block-${n}`, type: 'paragraph', content: [] },
372+
content: testBlock(`block-${n}`),
358373
isTopLevel: true,
359374
campaignId,
360375
shareStatus,

convex/blockShares/__tests__/blockShares.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,13 @@ import {
1111
createBlockShare,
1212
createNote,
1313
createSidebarShare,
14+
testBlock,
1415
} from '../../_test/factories.helper'
1516
import { expectPermissionDenied } from '../../_test/assertions.helper'
1617
import { api } from '../../_generated/api'
1718
import type { NoteWithContent } from '../../notes/types'
1819

19-
const BLOCK_CONTENT = {
20-
id: 'test-block-1',
21-
type: 'paragraph' as const,
22-
content: [],
23-
}
20+
const BLOCK_CONTENT = testBlock('test-block-1')
2421

2522
describe('setBlocksShareStatus', () => {
2623
const t = createTestContext()

convex/blockShares/__tests__/blockSharingWorkflows.test.ts

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { describe, expect, it } from 'vitest'
22
import { createTestContext } from '../../_test/setup.helper'
33
import { asDm, setupMultiPlayerContext } from '../../_test/identities.helper'
4-
import { createBlock, createNote, createSidebarShare } from '../../_test/factories.helper'
4+
import {
5+
createBlock,
6+
createNote,
7+
createSidebarShare,
8+
testBlock,
9+
} from '../../_test/factories.helper'
510
import { api } from '../../_generated/api'
611

712
describe('block sharing workflows', () => {
@@ -31,16 +36,8 @@ describe('block sharing workflows', () => {
3136
campaignMemberId: p2.memberId,
3237
})
3338

34-
const blockContent1 = {
35-
id: block1.blockId,
36-
type: 'paragraph' as const,
37-
content: [],
38-
}
39-
const blockContent2 = {
40-
id: block2.blockId,
41-
type: 'paragraph' as const,
42-
content: [],
43-
}
39+
const blockContent1 = testBlock(block1.blockId)
40+
const blockContent2 = testBlock(block2.blockId)
4441

4542
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
4643
campaignId: ctx.campaignId,
@@ -131,11 +128,7 @@ describe('block sharing workflows', () => {
131128
campaignMemberId: p1.memberId,
132129
})
133130

134-
const blockContent = {
135-
id: block.blockId,
136-
type: 'paragraph' as const,
137-
content: [],
138-
}
131+
const blockContent = testBlock(block.blockId)
139132

140133
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
141134
campaignId: ctx.campaignId,
@@ -248,11 +241,7 @@ describe('block sharing workflows', () => {
248241
campaignMemberId: p1.memberId,
249242
})
250243

251-
const blockContent = {
252-
id: block.blockId,
253-
type: 'paragraph' as const,
254-
content: [],
255-
}
244+
const blockContent = testBlock(block.blockId)
256245

257246
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
258247
campaignId: ctx.campaignId,
@@ -308,11 +297,7 @@ describe('block sharing workflows', () => {
308297
const note = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
309298
const block = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id)
310299

311-
const blockContent = {
312-
id: block.blockId,
313-
type: 'paragraph' as const,
314-
content: [],
315-
}
300+
const blockContent = testBlock(block.blockId)
316301

317302
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
318303
campaignId: ctx.campaignId,

convex/blockShares/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { CustomBlock } from '../notes/editorSpecs'
21
import type { Id } from '../_generated/dataModel'
2+
import type { CustomBlock } from '../notes/editorSpecs'
33

44
export type BlockShare = {
55
_id: Id<'blockShares'>

0 commit comments

Comments
 (0)