Barazo lexicon schemas and TypeScript types barazo.forum
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(lexicons): add reaction tokens, category format, and communityRef (#36)

Add knownValues with token definitions (like, heart, thumbsup) to the
reaction type field following AT Protocol conventions. Annotate the
category field with format: "record-key" for machine-readable semantics.
Populate the previously empty defs.json with a reusable communityRef
type. Regenerate TypeScript types, update Zod validation schemas, and
add corresponding tests. Bump version to 0.1.2.

All changes are non-breaking -- backward compatibility tests pass.

authored by

Guido X Jansen and committed by
GitHub
25dad2c4 75fb9634

+228 -18
+11 -2
lexicons/forum/barazo/defs.json
··· 1 1 { 2 2 "lexicon": 1, 3 3 "id": "forum.barazo.defs", 4 - "description": "Shared type definitions for Barazo forum lexicons. Reserved for future reusable types.", 5 - "defs": {} 4 + "description": "Shared type definitions for Barazo forum lexicons.", 5 + "defs": { 6 + "communityRef": { 7 + "type": "object", 8 + "description": "Reference to a Barazo community by its DID.", 9 + "required": ["did"], 10 + "properties": { 11 + "did": { "type": "string", "format": "did", "description": "The community's DID." } 12 + } 13 + } 14 + } 6 15 }
+10 -3
lexicons/forum/barazo/interaction/reaction.json
··· 18 18 }, 19 19 "type": { 20 20 "type": "string", 21 - "minLength": 1, 21 + "knownValues": [ 22 + "forum.barazo.interaction.reaction#like", 23 + "forum.barazo.interaction.reaction#heart", 24 + "forum.barazo.interaction.reaction#thumbsup" 25 + ], 22 26 "maxLength": 300, 23 27 "maxGraphemes": 30, 24 - "description": "Reaction type (e.g., 'like', 'heart', 'thumbsup'). Must match community's configured reaction set." 28 + "description": "Reaction type identifier. Communities may define additional values." 25 29 }, 26 30 "community": { 27 31 "type": "string", ··· 35 39 } 36 40 } 37 41 } 38 - } 42 + }, 43 + "like": { "type": "token", "description": "Simple approval reaction." }, 44 + "heart": { "type": "token", "description": "Love/appreciation reaction." }, 45 + "thumbsup": { "type": "token", "description": "Agreement/thumbs-up reaction." } 39 46 } 40 47 }
+2 -1
lexicons/forum/barazo/topic/post.json
··· 36 36 }, 37 37 "category": { 38 38 "type": "string", 39 + "format": "record-key", 39 40 "maxLength": 640, 40 41 "maxGraphemes": 64, 41 - "description": "Category rkey within the community." 42 + "description": "Category record key (slug) within the community. Follows AT Protocol record key syntax." 42 43 }, 43 44 "tags": { 44 45 "type": "array",
+1 -1
package.json
··· 1 1 { 2 2 "name": "@barazo-forum/lexicons", 3 - "version": "0.1.1", 3 + "version": "0.1.2", 4 4 "description": "AT Protocol lexicon schemas and generated TypeScript types for the Barazo forum platform", 5 5 "type": "module", 6 6 "packageManager": "pnpm@10.29.2",
+2
scripts/fixup-generated.js
··· 18 18 * The XRPC server/client wrappers generated by lex-cli are replaced with 19 19 * direct type re-exports since we use @atproto/api for PDS interactions. 20 20 */ 21 + export * as AppBskyRichtextFacet from "./types/app/bsky/richtext/facet.js"; 21 22 export * as ComAtprotoLabelDefs from "./types/com/atproto/label/defs.js"; 22 23 export * as ComAtprotoRepoStrongRef from "./types/com/atproto/repo/strongRef.js"; 23 24 export * as ForumBarazoActorPreferences from "./types/forum/barazo/actor/preferences.js"; 24 25 export * as ForumBarazoDefs from "./types/forum/barazo/defs.js"; 25 26 export * as ForumBarazoInteractionReaction from "./types/forum/barazo/interaction/reaction.js"; 27 + export * as ForumBarazoInteractionVote from "./types/forum/barazo/interaction/vote.js"; 26 28 export * as ForumBarazoTopicPost from "./types/forum/barazo/topic/post.js"; 27 29 export * as ForumBarazoTopicReply from "./types/forum/barazo/topic/reply.js"; 28 30 export { schemas, validate } from "./lexicons.js";
+36 -6
src/generated/lexicons.ts
··· 399 399 ForumBarazoDefs: { 400 400 lexicon: 1, 401 401 id: 'forum.barazo.defs', 402 - description: 403 - 'Shared type definitions for Barazo forum lexicons. Reserved for future reusable types.', 404 - defs: {}, 402 + description: 'Shared type definitions for Barazo forum lexicons.', 403 + defs: { 404 + communityRef: { 405 + type: 'object', 406 + description: 'Reference to a Barazo community by its DID.', 407 + required: ['did'], 408 + properties: { 409 + did: { 410 + type: 'string', 411 + format: 'did', 412 + description: "The community's DID.", 413 + }, 414 + }, 415 + }, 416 + }, 405 417 }, 406 418 ForumBarazoInteractionReaction: { 407 419 lexicon: 1, ··· 424 436 }, 425 437 type: { 426 438 type: 'string', 427 - minLength: 1, 439 + knownValues: [ 440 + 'forum.barazo.interaction.reaction#like', 441 + 'forum.barazo.interaction.reaction#heart', 442 + 'forum.barazo.interaction.reaction#thumbsup', 443 + ], 428 444 maxLength: 300, 429 445 maxGraphemes: 30, 430 446 description: 431 - "Reaction type (e.g., 'like', 'heart', 'thumbsup'). Must match community's configured reaction set.", 447 + 'Reaction type identifier. Communities may define additional values.', 432 448 }, 433 449 community: { 434 450 type: 'string', ··· 444 460 }, 445 461 }, 446 462 }, 463 + }, 464 + like: { 465 + type: 'token', 466 + description: 'Simple approval reaction.', 467 + }, 468 + heart: { 469 + type: 'token', 470 + description: 'Love/appreciation reaction.', 471 + }, 472 + thumbsup: { 473 + type: 'token', 474 + description: 'Agreement/thumbs-up reaction.', 447 475 }, 448 476 }, 449 477 }, ··· 530 558 }, 531 559 category: { 532 560 type: 'string', 561 + format: 'record-key', 533 562 maxLength: 640, 534 563 maxGraphemes: 64, 535 - description: 'Category rkey within the community.', 564 + description: 565 + 'Category record key (slug) within the community. Follows AT Protocol record key syntax.', 536 566 }, 537 567 tags: { 538 568 type: 'array',
+17
src/generated/types/forum/barazo/defs.ts
··· 9 9 const is$typed = _is$typed, 10 10 validate = _validate 11 11 const id = 'forum.barazo.defs' 12 + 13 + /** Reference to a Barazo community by its DID. */ 14 + export interface CommunityRef { 15 + $type?: 'forum.barazo.defs#communityRef' 16 + /** The community's DID. */ 17 + did: string 18 + } 19 + 20 + const hashCommunityRef = 'communityRef' 21 + 22 + export function isCommunityRef<V>(v: V) { 23 + return is$typed(v, id, hashCommunityRef) 24 + } 25 + 26 + export function validateCommunityRef<V>(v: V) { 27 + return validate<CommunityRef & V>(v, id, hashCommunityRef) 28 + }
+13 -2
src/generated/types/forum/barazo/interaction/reaction.ts
··· 18 18 export interface Main { 19 19 $type: 'forum.barazo.interaction.reaction' 20 20 subject: ComAtprotoRepoStrongRef.Main 21 - /** Reaction type (e.g., 'like', 'heart', 'thumbsup'). Must match community's configured reaction set. */ 22 - type: string 21 + /** Reaction type identifier. Communities may define additional values. */ 22 + type: 23 + | 'forum.barazo.interaction.reaction#like' 24 + | 'forum.barazo.interaction.reaction#heart' 25 + | 'forum.barazo.interaction.reaction#thumbsup' 26 + | (string & {}) 23 27 /** DID of the community where this reaction was created. Immutable origin identifier. */ 24 28 community: string 25 29 /** Client-declared timestamp when this reaction was originally created. */ ··· 42 46 isMain as isRecord, 43 47 validateMain as validateRecord, 44 48 } 49 + 50 + /** Simple approval reaction. */ 51 + export const LIKE = `${id}#like` 52 + /** Love/appreciation reaction. */ 53 + export const HEART = `${id}#heart` 54 + /** Agreement/thumbs-up reaction. */ 55 + export const THUMBSUP = `${id}#thumbsup`
+1 -1
src/generated/types/forum/barazo/topic/post.ts
··· 26 26 contentFormat?: 'markdown' | (string & {}) 27 27 /** DID of the community where this record was created. Immutable origin identifier for cross-community attribution. */ 28 28 community: string 29 - /** Category rkey within the community. */ 29 + /** Category record key (slug) within the community. Follows AT Protocol record key syntax. */ 30 30 category: string 31 31 /** Topic tags. Lowercase alphanumeric + hyphens. */ 32 32 tags?: string[]
+3
src/index.ts
··· 14 14 ForumBarazoInteractionReaction, 15 15 ForumBarazoInteractionVote, 16 16 ForumBarazoActorPreferences, 17 + ForumBarazoDefs, 17 18 ComAtprotoRepoStrongRef, 18 19 ComAtprotoLabelDefs, 19 20 } from './generated/index.js' ··· 34 35 selfLabelsSchema, 35 36 selfLabelSchema, 36 37 facetSchema, 38 + communityRefSchema, 37 39 type TopicPostInput, 38 40 type TopicReplyInput, 39 41 type ReactionInput, 40 42 type VoteInput, 41 43 type ActorPreferencesInput, 44 + type CommunityRefInput, 42 45 } from './validation/index.js' 43 46 44 47 // Lexicon ID constants
+13
src/validation/community-ref.ts
··· 1 + import { z } from 'zod' 2 + import { didRegex } from './patterns.js' 3 + 4 + /** 5 + * Zod schema for forum.barazo.defs#communityRef. 6 + * 7 + * Reference to a Barazo community by its DID. 8 + */ 9 + export const communityRefSchema = z.object({ 10 + did: z.string().regex(didRegex), 11 + }) 12 + 13 + export type CommunityRefInput = z.input<typeof communityRefSchema>
+1
src/validation/index.ts
··· 10 10 export { strongRefSchema } from './strong-ref.js' 11 11 export { selfLabelsSchema, selfLabelSchema } from './self-labels.js' 12 12 export { facetSchema } from './facet.js' 13 + export { communityRefSchema, type CommunityRefInput } from './community-ref.js'
+3
src/validation/patterns.ts
··· 1 1 /** DID format regex (simplified, catches obvious malformed DIDs). */ 2 2 export const didRegex = /^did:[a-z]+:[a-zA-Z0-9._:%-]+$/ 3 + 4 + /** AT Protocol record key format regex. Excludes "." and ".." per spec. */ 5 + export const recordKeyRegex = /^(?!\.{1,2}$)[a-zA-Z0-9._:~-]{1,512}$/
+2 -2
src/validation/topic-post.ts
··· 1 1 import { z } from 'zod' 2 2 import { selfLabelsSchema } from './self-labels.js' 3 3 import { facetSchema } from './facet.js' 4 - import { didRegex } from './patterns.js' 4 + import { didRegex, recordKeyRegex } from './patterns.js' 5 5 6 6 /** 7 7 * Zod schema for forum.barazo.topic.post records. ··· 17 17 content: z.string().min(1).max(100_000), 18 18 contentFormat: z.literal('markdown').optional(), 19 19 community: z.string().regex(didRegex), 20 - category: z.string().min(1).max(640), 20 + category: z.string().regex(recordKeyRegex).max(640), 21 21 tags: z.array(z.string().min(1).max(300)).max(5).optional(), 22 22 facets: z.array(facetSchema).optional(), 23 23 langs: z.array(z.string().min(1)).max(3).optional(),
+56
tests/lexicon-schemas.test.ts
··· 106 106 expect(labels['refs']).toContain('com.atproto.label.defs#selfLabels') 107 107 }) 108 108 109 + it('category uses record-key format', () => { 110 + const defs = schema['defs'] as Record<string, unknown> 111 + const main = defs['main'] as Record<string, unknown> 112 + const record = main['record'] as Record<string, unknown> 113 + const props = record['properties'] as Record<string, unknown> 114 + const category = props['category'] as Record<string, unknown> 115 + expect(category['type']).toBe('string') 116 + expect(category['format']).toBe('record-key') 117 + expect(category['maxLength']).toBe(640) 118 + expect(category['maxGraphemes']).toBe(64) 119 + }) 120 + 109 121 it('tags is optional with max 5 items', () => { 110 122 const defs = schema['defs'] as Record<string, unknown> 111 123 const main = defs['main'] as Record<string, unknown> ··· 230 242 expect(subject['type']).toBe('ref') 231 243 expect(subject['ref']).toBe('com.atproto.repo.strongRef') 232 244 }) 245 + 246 + it('type uses knownValues with token references', () => { 247 + const defs = schema['defs'] as Record<string, unknown> 248 + const main = defs['main'] as Record<string, unknown> 249 + const record = main['record'] as Record<string, unknown> 250 + const props = record['properties'] as Record<string, unknown> 251 + const type = props['type'] as Record<string, unknown> 252 + expect(type['knownValues']).toEqual([ 253 + 'forum.barazo.interaction.reaction#like', 254 + 'forum.barazo.interaction.reaction#heart', 255 + 'forum.barazo.interaction.reaction#thumbsup', 256 + ]) 257 + }) 258 + 259 + it('defines like, heart, and thumbsup token defs', () => { 260 + const defs = schema['defs'] as Record<string, unknown> 261 + for (const token of ['like', 'heart', 'thumbsup']) { 262 + const def = defs[token] as Record<string, unknown> 263 + expect(def).toBeDefined() 264 + expect(def['type']).toBe('token') 265 + expect(def['description']).toBeTypeOf('string') 266 + } 267 + }) 233 268 }) 234 269 235 270 describe('forum.barazo.interaction.vote lexicon', () => { ··· 332 367 expect(collections).toContain('forum.barazo.interaction.reaction') 333 368 expect(collections).toContain('forum.barazo.interaction.vote') 334 369 expect(collections).toContain('forum.barazo.actor.preferences') 370 + }) 371 + }) 372 + 373 + describe('forum.barazo.defs lexicon', () => { 374 + let schema: Record<string, unknown> 375 + 376 + it('loads successfully', async () => { 377 + schema = (await loadJson(join(LEXICONS_DIR, 'defs.json'))) as Record<string, unknown> 378 + expect(schema['id']).toBe('forum.barazo.defs') 379 + }) 380 + 381 + it('defines communityRef as an object with required did field', () => { 382 + const defs = schema['defs'] as Record<string, unknown> 383 + const communityRef = defs['communityRef'] as Record<string, unknown> 384 + expect(communityRef).toBeDefined() 385 + expect(communityRef['type']).toBe('object') 386 + expect(communityRef['required']).toEqual(['did']) 387 + const props = communityRef['properties'] as Record<string, unknown> 388 + const did = props['did'] as Record<string, unknown> 389 + expect(did['type']).toBe('string') 390 + expect(did['format']).toBe('did') 335 391 }) 336 392 }) 337 393
+14
tests/type-generation.test.ts
··· 5 5 ForumBarazoInteractionReaction, 6 6 ForumBarazoInteractionVote, 7 7 ForumBarazoActorPreferences, 8 + ForumBarazoDefs, 8 9 LEXICON_IDS, 9 10 schemas, 10 11 ids, ··· 34 35 it('exports ForumBarazoActorPreferences with Record type and validators', () => { 35 36 expect(ForumBarazoActorPreferences.isRecord).toBeTypeOf('function') 36 37 expect(ForumBarazoActorPreferences.validateRecord).toBeTypeOf('function') 38 + }) 39 + 40 + it('exports ForumBarazoDefs with CommunityRef validators', () => { 41 + expect(ForumBarazoDefs.isCommunityRef).toBeTypeOf('function') 42 + expect(ForumBarazoDefs.validateCommunityRef).toBeTypeOf('function') 43 + }) 44 + 45 + it('exports reaction token constants', () => { 46 + expect(ForumBarazoInteractionReaction.LIKE).toBe('forum.barazo.interaction.reaction#like') 47 + expect(ForumBarazoInteractionReaction.HEART).toBe('forum.barazo.interaction.reaction#heart') 48 + expect(ForumBarazoInteractionReaction.THUMBSUP).toBe( 49 + 'forum.barazo.interaction.reaction#thumbsup' 50 + ) 37 51 }) 38 52 }) 39 53
+43
tests/zod-validation.test.ts
··· 5 5 reactionSchema, 6 6 voteSchema, 7 7 actorPreferencesSchema, 8 + communityRefSchema, 8 9 } from '../src/validation/index.js' 9 10 10 11 const VALID_DID = 'did:plc:abc123def456' ··· 84 85 85 86 it('rejects empty category', () => { 86 87 expect(topicPostSchema.safeParse({ ...validPost, category: '' }).success).toBe(false) 88 + }) 89 + 90 + it('accepts valid record-key category values', () => { 91 + for (const key of ['general', 'my-category', 'v1.0', 'test_key', 'a:b']) { 92 + expect(topicPostSchema.safeParse({ ...validPost, category: key }).success).toBe(true) 93 + } 94 + }) 95 + 96 + it('rejects invalid record-key category values', () => { 97 + for (const key of ['.', '..', 'has space', 'has/slash', 'has@at']) { 98 + expect(topicPostSchema.safeParse({ ...validPost, category: key }).success).toBe(false) 99 + } 87 100 }) 88 101 89 102 it('rejects missing required fields', () => { ··· 201 214 ) 202 215 }) 203 216 217 + it('accepts known token values', () => { 218 + for (const type of [ 219 + 'forum.barazo.interaction.reaction#like', 220 + 'forum.barazo.interaction.reaction#heart', 221 + 'forum.barazo.interaction.reaction#thumbsup', 222 + ]) { 223 + expect(reactionSchema.safeParse({ ...validReaction, type }).success).toBe(true) 224 + } 225 + }) 226 + 227 + it('accepts custom reaction type values (knownValues is open)', () => { 228 + expect( 229 + reactionSchema.safeParse({ ...validReaction, type: 'custom-community-reaction' }).success 230 + ).toBe(true) 231 + }) 232 + 204 233 it('rejects invalid subject (not a strongRef)', () => { 205 234 expect( 206 235 reactionSchema.safeParse({ ··· 316 345 ).toBe(false) 317 346 }) 318 347 }) 348 + 349 + describe('communityRefSchema', () => { 350 + it('accepts a valid community reference', () => { 351 + expect(communityRefSchema.safeParse({ did: VALID_DID }).success).toBe(true) 352 + }) 353 + 354 + it('rejects invalid DID format', () => { 355 + expect(communityRefSchema.safeParse({ did: 'not-a-did' }).success).toBe(false) 356 + }) 357 + 358 + it('rejects missing did field', () => { 359 + expect(communityRefSchema.safeParse({}).success).toBe(false) 360 + }) 361 + })