prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey

cleanup old files

Tyler e9acbecd ebdfa22f

+112 -4355
+4 -3
.gitignore
··· 1 - lib 2 - node_modules 3 - /.attest 1 + lib/ 2 + node_modules/ 3 + .attest/ 4 + generated/
+108 -94
aislop/plan-emit.md
··· 52 52 53 53 ```json 54 54 { 55 - "bin": { 56 - "prototypey": "./lib/cli/index.js" 57 - }, 58 - "scripts": { 59 - "codegen": "prototypey gen-inferred ./generated/inferred ./lexicons/**/*.json" 60 - } 55 + "bin": { 56 + "prototypey": "./lib/cli/index.js" 57 + }, 58 + "scripts": { 59 + "codegen": "prototypey gen-inferred ./generated/inferred ./lexicons/**/*.json" 60 + } 61 61 } 62 62 ``` 63 63 ··· 73 73 74 74 ```typescript 75 75 // Example output: generated/inferred/app/bsky/feed/post.ts 76 - import type { Infer } from 'prototypey' 77 - import schema from '../../../../lexicons/app/bsky/feed/post.json' with { type: 'json' } 76 + import type { Infer } from "prototypey"; 77 + import schema from "../../../../lexicons/app/bsky/feed/post.json" with { type: "json" }; 78 78 79 - export type Post = Infer<typeof schema> 79 + export type Post = Infer<typeof schema>; 80 80 81 81 // Minimal runtime helpers 82 - export const PostSchema = schema 82 + export const PostSchema = schema; 83 83 export const isPost = (v: unknown): v is Post => { 84 - return typeof v === 'object' && v !== null && '$type' in v && 85 - v.$type === 'app.bsky.feed.post' 86 - } 84 + return ( 85 + typeof v === "object" && 86 + v !== null && 87 + "$type" in v && 88 + v.$type === "app.bsky.feed.post" 89 + ); 90 + }; 87 91 ``` 88 92 89 93 Benefits: ··· 97 101 98 102 ```json 99 103 { 100 - "dependencies": { 101 - "@atproto/lexicon": "^0.3.0" 102 - }, 103 - "devDependencies": { 104 - "@atproto/lex-cli": "^0.9.1", 105 - "commander": "^12.0.0", 106 - "glob": "^10.0.0" 107 - }, 108 - "peerDependencies": { 109 - "typescript": ">=5.0.0" 110 - } 104 + "dependencies": { 105 + "@atproto/lexicon": "^0.3.0" 106 + }, 107 + "devDependencies": { 108 + "@atproto/lex-cli": "^0.9.1", 109 + "commander": "^12.0.0", 110 + "glob": "^10.0.0" 111 + }, 112 + "peerDependencies": { 113 + "typescript": ">=5.0.0" 114 + } 111 115 } 112 116 ``` 113 117 ··· 117 121 118 122 ```json 119 123 { 120 - "scripts": { 121 - "build": "tsdown", 122 - "build:cli": "tsdown --entry src/cli/index.ts --format esm --dts false", 123 - "codegen:samples": "prototypey gen-inferred ./generated/samples ./samples/*.json", 124 - "prepack": "pnpm build && pnpm build:cli" 125 - } 124 + "scripts": { 125 + "build": "tsdown", 126 + "build:cli": "tsdown --entry src/cli/index.ts --format esm --dts false", 127 + "codegen:samples": "prototypey gen-inferred ./generated/samples ./samples/*.json", 128 + "prepack": "pnpm build && pnpm build:cli" 129 + } 126 130 } 127 131 ``` 128 132 ··· 132 136 133 137 ```json 134 138 { 135 - "lexicons": "./lexicons", 136 - "output": { 137 - "inferred": "./generated/inferred", 138 - "types": "./generated/types" 139 - }, 140 - "include": ["**/*.json"], 141 - "exclude": ["**/node_modules/**"] 139 + "lexicons": "./lexicons", 140 + "output": { 141 + "inferred": "./generated/inferred", 142 + "types": "./generated/types" 143 + }, 144 + "include": ["**/*.json"], 145 + "exclude": ["**/node_modules/**"] 142 146 } 143 147 ``` 144 148 ··· 170 174 171 175 1. ✅ **Phase 1**: Basic CLI structure + Track B (inferred generation) - COMPLETE 172 176 2. ✅ **Phase 2**: File organization + output directory structure - COMPLETE 173 - 3. ✅ **Phase 3**: Convert to pnpm workspaces monorepo - COMPLETE 177 + 3. ✅ **Phase 3**: Convert to pnpm workspaces monorepo - COMPLETE - this was marked complete but we still have src and packages 174 178 4. **Phase 4**: Track A (standard generation, delegate to lex-cli) 175 179 5. **Phase 5**: Configuration file support 176 180 6. **Phase 6**: Documentation + examples ··· 180 184 ### ✅ Completed (2025-10-16) 181 185 182 186 **Tech Stack Choices:** 187 + 183 188 - Used `sade` instead of `commander` (modern, minimal CLI framework from awesome-e18e) 184 189 - Used `tinyglobby` instead of `glob` (faster, modern alternative) 185 190 - Built with `tsdown` for CLI bundling 186 191 187 192 **Structure Created:** 193 + 188 194 ``` 189 195 prototypey/ 190 196 ├── src/cli/ ··· 200 206 ``` 201 207 202 208 **Generated Code Pattern:** 209 + 203 210 ```typescript 204 211 // generated/inferred/app/bsky/actor/profile.ts 205 212 import type { Infer } from "prototypey"; ··· 211 218 ``` 212 219 213 220 **CLI Usage:** 221 + 214 222 ```bash 215 223 # Build CLI 216 224 pnpm build:cli ··· 223 231 ``` 224 232 225 233 **Key Features:** 234 + 226 235 - Converts NSID to file paths: `app.bsky.feed.post` → `app/bsky/feed/post.ts` 227 236 - Generates minimal runtime code with type inference 228 237 - Auto-creates directory structure ··· 230 239 - Type guard functions for runtime checks 231 240 232 241 **Testing:** 242 + 233 243 - Successfully generated types from sample lexicons 234 244 - Runtime validation works (tested with node) 235 245 - Schema imports work correctly with JSON modules ··· 281 291 ### Package Configurations 282 292 283 293 **Root `pnpm-workspace.yaml`:** 294 + 284 295 ```yaml 285 296 packages: 286 - - 'packages/*' 297 + - "packages/*" 287 298 ``` 288 299 289 300 **Root `package.json`:** 301 + 290 302 ```json 291 303 { 292 - "name": "prototypey-monorepo", 293 - "private": true, 294 - "scripts": { 295 - "build": "pnpm -r build", 296 - "test": "pnpm -r test", 297 - "lint": "pnpm -r lint", 298 - "format": "prettier . --write" 299 - } 304 + "name": "prototypey-monorepo", 305 + "private": true, 306 + "scripts": { 307 + "build": "pnpm -r build", 308 + "test": "pnpm -r test", 309 + "lint": "pnpm -r lint", 310 + "format": "prettier . --write" 311 + } 300 312 } 301 313 ``` 302 314 303 315 **`packages/prototypey/package.json`:** 316 + 304 317 ```json 305 318 { 306 - "name": "prototypey", 307 - "version": "0.0.0", 308 - "main": "lib/index.js", 309 - "exports": { 310 - ".": "./lib/index.js", 311 - "./infer": "./lib/infer.js" 312 - }, 313 - "dependencies": {}, 314 - "scripts": { 315 - "build": "tsdown", 316 - "test": "vitest run" 317 - } 319 + "name": "prototypey", 320 + "version": "0.0.0", 321 + "main": "lib/index.js", 322 + "exports": { 323 + ".": "./lib/index.js", 324 + "./infer": "./lib/infer.js" 325 + }, 326 + "dependencies": {}, 327 + "scripts": { 328 + "build": "tsdown", 329 + "test": "vitest run" 330 + } 318 331 } 319 332 ``` 320 333 321 334 **`packages/cli/package.json`:** 335 + 322 336 ```json 323 337 { 324 - "name": "@prototypey/cli", 325 - "version": "0.0.0", 326 - "bin": { 327 - "prototypey": "./lib/index.js" 328 - }, 329 - "dependencies": { 330 - "prototypey": "workspace:*", 331 - "sade": "^1.8.1", 332 - "tinyglobby": "^0.2.15" 333 - }, 334 - "scripts": { 335 - "build": "tsdown --entry src/index.ts --format esm --dts false" 336 - } 338 + "name": "@prototypey/cli", 339 + "version": "0.0.0", 340 + "bin": { 341 + "prototypey": "./lib/index.js" 342 + }, 343 + "dependencies": { 344 + "prototypey": "workspace:*", 345 + "sade": "^1.8.1", 346 + "tinyglobby": "^0.2.15" 347 + }, 348 + "scripts": { 349 + "build": "tsdown --entry src/index.ts --format esm --dts false" 350 + } 337 351 } 338 352 ``` 339 353 ··· 442 456 /** 443 457 * GENERATED CODE - DO NOT MODIFY 444 458 */ 445 - import { ValidationResult, BlobRef } from '@atproto/lexicon' 446 - import { lexicons } from '../../../../lexicons' 447 - import { isObj, hasProp } from '../../../../util' 448 - import { CID } from 'multiformats/cid' 459 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 460 + import { lexicons } from "../../../../lexicons"; 461 + import { isObj, hasProp } from "../../../../util"; 462 + import { CID } from "multiformats/cid"; 449 463 450 464 export interface Main { 451 - $type?: 'app.bsky.richtext.facet' 452 - index: ByteSlice 453 - features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] 454 - [k: string]: unknown 465 + $type?: "app.bsky.richtext.facet"; 466 + index: ByteSlice; 467 + features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[]; 468 + [k: string]: unknown; 455 469 } 456 470 457 471 export function isMain(v: unknown): v is Main { 458 - return ( 459 - isObj(v) && 460 - hasProp(v, '$type') && 461 - (v.$type === 'app.bsky.richtext.facet#main' || 462 - v.$type === 'app.bsky.richtext.facet') 463 - ) 472 + return ( 473 + isObj(v) && 474 + hasProp(v, "$type") && 475 + (v.$type === "app.bsky.richtext.facet#main" || 476 + v.$type === "app.bsky.richtext.facet") 477 + ); 464 478 } 465 479 466 480 export function validateMain(v: unknown): ValidationResult { 467 - return lexicons.validate('app.bsky.richtext.facet#main', v) 481 + return lexicons.validate("app.bsky.richtext.facet#main", v); 468 482 } 469 483 ``` 470 484 ··· 474 488 475 489 ```json 476 490 { 477 - "scripts": { 478 - "codegen": "lex gen-api --yes ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", 479 - "build": "tsc --build tsconfig.build.json" 480 - }, 481 - "devDependencies": { 482 - "@atproto/lex-cli": "^0.9.1" 483 - } 491 + "scripts": { 492 + "codegen": "lex gen-api --yes ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", 493 + "build": "tsc --build tsconfig.build.json" 494 + }, 495 + "devDependencies": { 496 + "@atproto/lex-cli": "^0.9.1" 497 + } 484 498 } 485 499 ``` 486 500
-71
src/cli/commands/gen-inferred.ts
··· 1 - import { glob } from "tinyglobby"; 2 - import { readFile, mkdir, writeFile } from "node:fs/promises"; 3 - import { join, dirname, relative, parse } from "node:path"; 4 - import { generateInferredCode } from "../templates/inferred.ts"; 5 - 6 - interface LexiconSchema { 7 - lexicon: number; 8 - id: string; 9 - defs: Record<string, unknown>; 10 - } 11 - 12 - export async function genInferred( 13 - outdir: string, 14 - schemas: string | string[], 15 - ): Promise<void> { 16 - try { 17 - const schemaPatterns = Array.isArray(schemas) ? schemas : [schemas]; 18 - 19 - // Find all schema files matching the patterns 20 - const schemaFiles = await glob(schemaPatterns, { 21 - absolute: true, 22 - onlyFiles: true, 23 - }); 24 - 25 - if (schemaFiles.length === 0) { 26 - console.log("No schema files found matching patterns:", schemaPatterns); 27 - return; 28 - } 29 - 30 - console.log(`Found ${schemaFiles.length} schema file(s)`); 31 - 32 - // Process each schema file 33 - for (const schemaPath of schemaFiles) { 34 - await processSchema(schemaPath, outdir); 35 - } 36 - 37 - console.log(`\nGenerated inferred types in ${outdir}`); 38 - } catch (error) { 39 - console.error("Error generating inferred types:", error); 40 - process.exit(1); 41 - } 42 - } 43 - 44 - async function processSchema( 45 - schemaPath: string, 46 - outdir: string, 47 - ): Promise<void> { 48 - const content = await readFile(schemaPath, "utf-8"); 49 - const schema: LexiconSchema = JSON.parse(content); 50 - 51 - if (!schema.id || !schema.defs) { 52 - console.warn(`Skipping ${schemaPath}: Missing id or defs`); 53 - return; 54 - } 55 - 56 - // Convert NSID to file path: app.bsky.feed.post -> app/bsky/feed/post.ts 57 - const nsidParts = schema.id.split("."); 58 - const relativePath = join(...nsidParts) + ".ts"; 59 - const outputPath = join(outdir, relativePath); 60 - 61 - // Create directory structure 62 - await mkdir(dirname(outputPath), { recursive: true }); 63 - 64 - // Generate the TypeScript code 65 - const code = generateInferredCode(schema, schemaPath, outdir); 66 - 67 - // Write the file 68 - await writeFile(outputPath, code, "utf-8"); 69 - 70 - console.log(` ✓ ${schema.id} -> ${relativePath}`); 71 - }
-17
src/cli/index.ts
··· 1 - #!/usr/bin/env node 2 - import sade from "sade"; 3 - import { genInferred } from "./commands/gen-inferred.ts"; 4 - 5 - const prog = sade("prototypey"); 6 - 7 - prog 8 - .version("0.0.0") 9 - .describe("Type-safe lexicon inference and code generation"); 10 - 11 - prog 12 - .command("gen-inferred <outdir> <schemas...>") 13 - .describe("Generate type-inferred code from lexicon schemas") 14 - .example("gen-inferred ./generated/inferred ./lexicons/**/*.json") 15 - .action(genInferred); 16 - 17 - prog.parse(process.argv);
-65
src/cli/templates/inferred.ts
··· 1 - import { relative, dirname } from "node:path"; 2 - 3 - interface LexiconSchema { 4 - lexicon: number; 5 - id: string; 6 - defs: Record<string, unknown>; 7 - } 8 - 9 - export function generateInferredCode( 10 - schema: LexiconSchema, 11 - schemaPath: string, 12 - outdir: string, 13 - ): string { 14 - const { id } = schema; 15 - 16 - // Calculate relative import path from output file to schema file 17 - // We need to go from generated/{nsid}.ts to the original schema 18 - const nsidParts = id.split("."); 19 - const outputDir = dirname([outdir, ...nsidParts].join("/")); 20 - const relativeSchemaPath = relative(outputDir, schemaPath); 21 - 22 - // Generate a clean type name from the NSID 23 - const typeName = generateTypeName(id); 24 - 25 - return `// Generated by prototypey - DO NOT EDIT 26 - // Source: ${id} 27 - import type { Infer } from "prototypey"; 28 - import schema from "${relativeSchemaPath}" with { type: "json" }; 29 - 30 - /** 31 - * Type-inferred from lexicon schema: ${id} 32 - */ 33 - export type ${typeName} = Infer<typeof schema>; 34 - 35 - /** 36 - * The lexicon schema object 37 - */ 38 - export const ${typeName}Schema = schema; 39 - 40 - /** 41 - * Type guard to check if a value is a ${typeName} 42 - */ 43 - export function is${typeName}(v: unknown): v is ${typeName} { 44 - return ( 45 - typeof v === "object" && 46 - v !== null && 47 - "$type" in v && 48 - v.$type === "${id}" 49 - ); 50 - } 51 - `; 52 - } 53 - 54 - function generateTypeName(nsid: string): string { 55 - // Convert app.bsky.feed.post -> Post 56 - // Convert com.atproto.repo.createRecord -> CreateRecord 57 - const parts = nsid.split("."); 58 - const lastPart = parts[parts.length - 1]; 59 - 60 - // Convert kebab-case or camelCase to PascalCase 61 - return lastPart 62 - .split(/[-_]/) 63 - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 64 - .join(""); 65 - }
-2
src/index.ts
··· 1 - export * from "./lib.ts"; 2 - export * from "./infer.ts";
-141
src/infer.ts
··· 1 - import { Prettify } from "./type-utils.ts"; 2 - 3 - /* eslint-disable @typescript-eslint/no-empty-object-type */ 4 - type InferType<T> = T extends { type: "record" } 5 - ? InferRecord<T> 6 - : T extends { type: "object" } 7 - ? InferObject<T> 8 - : T extends { type: "array" } 9 - ? InferArray<T> 10 - : T extends { type: "params" } 11 - ? InferParams<T> 12 - : T extends { type: "union" } 13 - ? InferUnion<T> 14 - : T extends { type: "token" } 15 - ? InferToken<T> 16 - : T extends { type: "ref" } 17 - ? InferRef<T> 18 - : T extends { type: "unknown" } 19 - ? unknown 20 - : T extends { type: "null" } 21 - ? null 22 - : T extends { type: "boolean" } 23 - ? boolean 24 - : T extends { type: "integer" } 25 - ? number 26 - : T extends { type: "string" } 27 - ? string 28 - : T extends { type: "bytes" } 29 - ? Uint8Array 30 - : T extends { type: "cid-link" } 31 - ? string 32 - : T extends { type: "blob" } 33 - ? Blob 34 - : never; 35 - 36 - type InferToken<T> = T extends { enum: readonly (infer U)[] } ? U : string; 37 - 38 - export type GetRequired<T> = T extends { required: readonly (infer R)[] } 39 - ? R 40 - : never; 41 - export type GetNullable<T> = T extends { nullable: readonly (infer N)[] } 42 - ? N 43 - : never; 44 - 45 - type InferObject< 46 - T, 47 - Nullable extends string = GetNullable<T> & string, 48 - Required extends string = GetRequired<T> & string, 49 - NullableAndRequired extends string = Required & Nullable & string, 50 - Normal extends string = "properties" extends keyof T 51 - ? Exclude<keyof T["properties"], Required | Nullable> & string 52 - : never, 53 - > = Prettify< 54 - T extends { properties: infer P } 55 - ? { 56 - -readonly [K in Normal]?: InferType<P[K & keyof P]>; 57 - } & { 58 - -readonly [K in Exclude<Required, NullableAndRequired>]-?: InferType< 59 - P[K & keyof P] 60 - >; 61 - } & { 62 - -readonly [K in Exclude<Nullable, NullableAndRequired>]?: InferType< 63 - P[K & keyof P] 64 - > | null; 65 - } & { 66 - -readonly [K in NullableAndRequired]: InferType<P[K & keyof P]> | null; 67 - } 68 - : {} 69 - >; 70 - 71 - type InferArray<T> = T extends { items: infer Items } 72 - ? InferType<Items>[] 73 - : never[]; 74 - 75 - type InferUnion<T> = T extends { refs: readonly (infer R)[] } 76 - ? R extends string 77 - ? { $type: R; [key: string]: unknown } 78 - : never 79 - : never; 80 - 81 - type InferRef<T> = T extends { ref: infer R } 82 - ? R extends string 83 - ? { $type: R; [key: string]: unknown } 84 - : unknown 85 - : unknown; 86 - 87 - type InferParams<T> = InferObject<T>; 88 - 89 - type InferRecord<T> = T extends { record: infer R } 90 - ? R extends { type: "object" } 91 - ? InferObject<R> 92 - : R extends { type: "union" } 93 - ? InferUnion<R> 94 - : unknown 95 - : unknown; 96 - 97 - /** 98 - * Recursively replaces stub references in a type with their actual definitions. 99 - * Detects circular references and missing references, returning string literal error messages. 100 - */ 101 - type ReplaceRefsInType<T, Defs, Visited = never> = 102 - // Check if this is a ref stub type (has $type starting with #) 103 - T extends { $type: `#${infer DefName}` } 104 - ? DefName extends keyof Defs 105 - ? // Check for circular reference 106 - DefName extends Visited 107 - ? `[Circular reference detected: #${DefName}]` 108 - : // Recursively resolve the ref and preserve the $type marker 109 - Prettify< 110 - ReplaceRefsInType<Defs[DefName], Defs, Visited | DefName> & { 111 - $type: T["$type"]; 112 - } 113 - > 114 - : // Reference not found in definitions 115 - `[Reference not found: #${DefName}]` 116 - : // Handle arrays (but not Uint8Array or other typed arrays) 117 - T extends Uint8Array | Blob 118 - ? T 119 - : T extends readonly (infer Item)[] 120 - ? ReplaceRefsInType<Item, Defs, Visited>[] 121 - : // Handle plain objects (exclude built-in types and functions) 122 - T extends object 123 - ? T extends (...args: unknown[]) => unknown 124 - ? T 125 - : { [K in keyof T]: ReplaceRefsInType<T[K], Defs, Visited> } 126 - : // Primitives pass through unchanged 127 - T; 128 - 129 - /** 130 - * Infers the TypeScript type for a lexicon namespace, returning only the 'main' definition 131 - * with all local refs (#user, #post, etc.) resolved to their actual types. 132 - */ 133 - export type Infer<T extends { id: string; defs: Record<string, unknown> }> = 134 - Prettify< 135 - "main" extends keyof T["defs"] 136 - ? { $type: T["id"] } & ReplaceRefsInType< 137 - InferType<T["defs"]["main"]>, 138 - { [K in keyof T["defs"]]: InferType<T["defs"][K]> } 139 - > 140 - : never 141 - >;
-579
src/lib.ts
··· 1 - /* eslint-disable @typescript-eslint/no-empty-object-type */ 2 - import type { Infer } from "./infer.ts"; 3 - import type { UnionToTuple } from "./type-utils.ts"; 4 - 5 - /** @see https://atproto.com/specs/lexicon#overview-of-types */ 6 - type LexiconType = 7 - // Concrete types 8 - | "null" 9 - | "boolean" 10 - | "integer" 11 - | "string" 12 - | "bytes" 13 - | "cid-link" 14 - | "blob" 15 - // Container types 16 - | "array" 17 - | "object" 18 - | "params" 19 - // Meta types 20 - | "token" 21 - | "ref" 22 - | "union" 23 - | "unknown" 24 - // Primary types 25 - | "record" 26 - | "query" 27 - | "procedure" 28 - | "subscription"; 29 - 30 - /** 31 - * Common options available for lexicon items. 32 - * @see https://atproto.com/specs/lexicon#string-formats 33 - */ 34 - interface LexiconItemCommonOptions { 35 - /** Indicates this field must be provided */ 36 - required?: boolean; 37 - /** Indicates this field can be explicitly set to null */ 38 - nullable?: boolean; 39 - } 40 - 41 - /** 42 - * Base interface for all lexicon items. 43 - * @see https://atproto.com/specs/lexicon#overview-of-types 44 - */ 45 - interface LexiconItem extends LexiconItemCommonOptions { 46 - type: LexiconType; 47 - } 48 - 49 - /** 50 - * Definition in a lexicon namespace. 51 - * @see https://atproto.com/specs/lexicon#lexicon-document 52 - */ 53 - interface Def { 54 - type: LexiconType; 55 - } 56 - 57 - /** 58 - * Lexicon namespace document structure. 59 - * @see https://atproto.com/specs/lexicon#lexicon-document 60 - */ 61 - interface LexiconNamespace { 62 - /** Namespaced identifier (NSID) for this lexicon */ 63 - id: string; 64 - /** Named definitions within this namespace */ 65 - defs: Record<string, Def>; 66 - } 67 - 68 - /** 69 - * String type options. 70 - * @see https://atproto.com/specs/lexicon#string 71 - */ 72 - interface StringOptions extends LexiconItemCommonOptions { 73 - /** 74 - * Semantic string format constraint. 75 - * @see https://atproto.com/specs/lexicon#string-formats 76 - */ 77 - format?: 78 - | "at-identifier" // Handle or DID 79 - | "at-uri" // AT Protocol URI 80 - | "cid" // Content Identifier 81 - | "datetime" // Timestamp (UTC, ISO 8601) 82 - | "did" // Decentralized Identifier 83 - | "handle" // User handle identifier 84 - | "nsid" // Namespaced Identifier 85 - | "tid" // Timestamp Identifier 86 - | "record-key" // Repository record key 87 - | "uri" // Generic URI 88 - | "language"; // IETF BCP 47 language tag 89 - /** Maximum string length in bytes */ 90 - maxLength?: number; 91 - /** Minimum string length in bytes */ 92 - minLength?: number; 93 - /** Maximum string length in Unicode graphemes */ 94 - maxGraphemes?: number; 95 - /** Minimum string length in Unicode graphemes */ 96 - minGraphemes?: number; 97 - /** Hints at expected values, not enforced */ 98 - knownValues?: string[]; 99 - /** Restricts to an exact set of string values */ 100 - enum?: string[]; 101 - /** Default value if not provided */ 102 - default?: string; 103 - /** Fixed, unchangeable value */ 104 - const?: string; 105 - } 106 - 107 - /** 108 - * Boolean type options. 109 - * @see https://atproto.com/specs/lexicon#boolean 110 - */ 111 - interface BooleanOptions extends LexiconItemCommonOptions { 112 - /** Default value if not provided */ 113 - default?: boolean; 114 - /** Fixed, unchangeable value */ 115 - const?: boolean; 116 - } 117 - 118 - /** 119 - * Integer type options. 120 - * @see https://atproto.com/specs/lexicon#integer 121 - */ 122 - interface IntegerOptions extends LexiconItemCommonOptions { 123 - /** Minimum allowed value (inclusive) */ 124 - minimum?: number; 125 - /** Maximum allowed value (inclusive) */ 126 - maximum?: number; 127 - /** Restricts to an exact set of integer values */ 128 - enum?: number[]; 129 - /** Default value if not provided */ 130 - default?: number; 131 - /** Fixed, unchangeable value */ 132 - const?: number; 133 - } 134 - 135 - /** 136 - * Bytes type options for arbitrary byte arrays. 137 - * @see https://atproto.com/specs/lexicon#bytes 138 - */ 139 - interface BytesOptions extends LexiconItemCommonOptions { 140 - /** Minimum byte array length */ 141 - minLength?: number; 142 - /** Maximum byte array length */ 143 - maxLength?: number; 144 - } 145 - 146 - /** 147 - * Blob type options for binary data with MIME types. 148 - * @see https://atproto.com/specs/lexicon#blob 149 - */ 150 - interface BlobOptions extends LexiconItemCommonOptions { 151 - /** Allowed MIME types (e.g., ["image/png", "image/jpeg"]) */ 152 - accept?: string[]; 153 - /** Maximum blob size in bytes */ 154 - maxSize?: number; 155 - } 156 - 157 - /** 158 - * Array type options. 159 - * @see https://atproto.com/specs/lexicon#array 160 - */ 161 - interface ArrayOptions extends LexiconItemCommonOptions { 162 - /** Minimum array length */ 163 - minLength?: number; 164 - /** Maximum array length */ 165 - maxLength?: number; 166 - } 167 - 168 - /** 169 - * Record type options for repository records. 170 - * @see https://atproto.com/specs/lexicon#record 171 - */ 172 - interface RecordOptions { 173 - /** Record key strategy: "self" for self-describing or "tid" for timestamp IDs */ 174 - key: "self" | "tid"; 175 - /** Object schema defining the record structure */ 176 - record: { type: "object" }; 177 - /** Human-readable description */ 178 - description?: string; 179 - } 180 - 181 - /** 182 - * Union type options for multiple possible types. 183 - * @see https://atproto.com/specs/lexicon#union 184 - */ 185 - interface UnionOptions extends LexiconItemCommonOptions { 186 - /** If true, only listed refs are allowed; if false, additional types may be added */ 187 - closed?: boolean; 188 - } 189 - 190 - /** 191 - * Map of property names to their lexicon item definitions. 192 - * @see https://atproto.com/specs/lexicon#object 193 - */ 194 - type ObjectProperties = Record< 195 - string, 196 - { 197 - type: LexiconType; 198 - } 199 - >; 200 - 201 - type RequiredKeys<T> = { 202 - [K in keyof T]: T[K] extends { required: true } ? K : never; 203 - }[keyof T]; 204 - 205 - type NullableKeys<T> = { 206 - [K in keyof T]: T[K] extends { nullable: true } ? K : never; 207 - }[keyof T]; 208 - 209 - /** 210 - * Resulting object schema with required and nullable fields extracted. 211 - * @see https://atproto.com/specs/lexicon#object 212 - */ 213 - type ObjectResult<T extends ObjectProperties> = { 214 - type: "object"; 215 - /** Property definitions */ 216 - properties: { 217 - [K in keyof T]: T[K] extends { type: "object" } 218 - ? T[K] 219 - : Omit<T[K], "required" | "nullable">; 220 - }; 221 - } & ([RequiredKeys<T>] extends [never] 222 - ? {} 223 - : { required: UnionToTuple<RequiredKeys<T>> }) & 224 - ([NullableKeys<T>] extends [never] 225 - ? {} 226 - : { nullable: UnionToTuple<NullableKeys<T>> }); 227 - 228 - /** 229 - * Map of parameter names to their lexicon item definitions. 230 - * @see https://atproto.com/specs/lexicon#params 231 - */ 232 - type ParamsProperties = Record<string, LexiconItem>; 233 - 234 - /** 235 - * Resulting params schema with required fields extracted. 236 - * @see https://atproto.com/specs/lexicon#params 237 - */ 238 - type ParamsResult<T extends ParamsProperties> = { 239 - type: "params"; 240 - /** Parameter definitions */ 241 - properties: { 242 - [K in keyof T]: Omit<T[K], "required" | "nullable">; 243 - }; 244 - } & ([RequiredKeys<T>] extends [never] 245 - ? {} 246 - : { required: UnionToTuple<RequiredKeys<T>> }); 247 - 248 - /** 249 - * HTTP request or response body schema. 250 - * @see https://atproto.com/specs/lexicon#http-endpoints 251 - */ 252 - interface BodySchema { 253 - /** MIME type encoding (typically "application/json") */ 254 - encoding: "application/json" | (string & {}); 255 - /** Human-readable description */ 256 - description?: string; 257 - /** Object schema defining the body structure */ 258 - schema?: ObjectResult<ObjectProperties>; 259 - } 260 - 261 - /** 262 - * Error definition for HTTP endpoints. 263 - * @see https://atproto.com/specs/lexicon#http-endpoints 264 - */ 265 - interface ErrorDef { 266 - /** Error name/code */ 267 - name: string; 268 - /** Human-readable error description */ 269 - description?: string; 270 - } 271 - 272 - /** 273 - * Query endpoint options (HTTP GET). 274 - * @see https://atproto.com/specs/lexicon#query 275 - */ 276 - interface QueryOptions { 277 - /** Human-readable description */ 278 - description?: string; 279 - /** Query string parameters */ 280 - parameters?: ParamsResult<ParamsProperties>; 281 - /** Response body schema */ 282 - output?: BodySchema; 283 - /** Possible error responses */ 284 - errors?: ErrorDef[]; 285 - } 286 - 287 - /** 288 - * Procedure endpoint options (HTTP POST). 289 - * @see https://atproto.com/specs/lexicon#procedure 290 - */ 291 - interface ProcedureOptions { 292 - /** Human-readable description */ 293 - description?: string; 294 - /** Query string parameters */ 295 - parameters?: ParamsResult<ParamsProperties>; 296 - /** Request body schema */ 297 - input?: BodySchema; 298 - /** Response body schema */ 299 - output?: BodySchema; 300 - /** Possible error responses */ 301 - errors?: ErrorDef[]; 302 - } 303 - 304 - /** 305 - * WebSocket message schema for subscriptions. 306 - * @see https://atproto.com/specs/lexicon#subscription 307 - */ 308 - interface MessageSchema { 309 - /** Human-readable description */ 310 - description?: string; 311 - /** Union of possible message types */ 312 - schema: { type: "union"; refs: readonly string[] }; 313 - } 314 - 315 - /** 316 - * Subscription endpoint options (WebSocket). 317 - * @see https://atproto.com/specs/lexicon#subscription 318 - */ 319 - interface SubscriptionOptions { 320 - /** Human-readable description */ 321 - description?: string; 322 - /** Query string parameters */ 323 - parameters?: ParamsResult<ParamsProperties>; 324 - /** Message schema for events */ 325 - message?: MessageSchema; 326 - /** Possible error responses */ 327 - errors?: ErrorDef[]; 328 - } 329 - 330 - class Namespace<T extends LexiconNamespace> { 331 - public json: T; 332 - public infer: Infer<T> = null as unknown as Infer<T>; 333 - 334 - constructor(json: T) { 335 - this.json = json; 336 - } 337 - } 338 - 339 - /** 340 - * Main API for creating lexicon schemas. 341 - * @see https://atproto.com/specs/lexicon 342 - */ 343 - export const lx = { 344 - /** 345 - * Creates a null type. 346 - * @see https://atproto.com/specs/lexicon#null 347 - */ 348 - null( 349 - options?: LexiconItemCommonOptions, 350 - ): { type: "null" } & LexiconItemCommonOptions { 351 - return { 352 - type: "null", 353 - ...options, 354 - }; 355 - }, 356 - /** 357 - * Creates a boolean type with optional constraints. 358 - * @see https://atproto.com/specs/lexicon#boolean 359 - */ 360 - boolean<T extends BooleanOptions>(options?: T): T & { type: "boolean" } { 361 - return { 362 - type: "boolean", 363 - ...options, 364 - } as T & { type: "boolean" }; 365 - }, 366 - /** 367 - * Creates an integer type with optional min/max and enum constraints. 368 - * @see https://atproto.com/specs/lexicon#integer 369 - */ 370 - integer<T extends IntegerOptions>(options?: T): T & { type: "integer" } { 371 - return { 372 - type: "integer", 373 - ...options, 374 - } as T & { type: "integer" }; 375 - }, 376 - /** 377 - * Creates a string type with optional format, length, and value constraints. 378 - * @see https://atproto.com/specs/lexicon#string 379 - */ 380 - string<T extends StringOptions>(options?: T): T & { type: "string" } { 381 - return { 382 - type: "string", 383 - ...options, 384 - } as T & { type: "string" }; 385 - }, 386 - /** 387 - * Creates an unknown type for flexible, unvalidated objects. 388 - * @see https://atproto.com/specs/lexicon#unknown 389 - */ 390 - unknown( 391 - options?: LexiconItemCommonOptions, 392 - ): { type: "unknown" } & LexiconItemCommonOptions { 393 - return { 394 - type: "unknown", 395 - ...options, 396 - }; 397 - }, 398 - /** 399 - * Creates a bytes type for arbitrary byte arrays. 400 - * @see https://atproto.com/specs/lexicon#bytes 401 - */ 402 - bytes<T extends BytesOptions>(options?: T): T & { type: "bytes" } { 403 - return { 404 - type: "bytes", 405 - ...options, 406 - } as T & { type: "bytes" }; 407 - }, 408 - /** 409 - * Creates a CID link reference to content-addressed data. 410 - * @see https://atproto.com/specs/lexicon#cid-link 411 - */ 412 - cidLink<Link extends string>(link: Link): { type: "cid-link"; $link: Link } { 413 - return { 414 - type: "cid-link", 415 - $link: link, 416 - }; 417 - }, 418 - /** 419 - * Creates a blob type for binary data with MIME type constraints. 420 - * @see https://atproto.com/specs/lexicon#blob 421 - */ 422 - blob<T extends BlobOptions>(options?: T): T & { type: "blob" } { 423 - return { 424 - type: "blob", 425 - ...options, 426 - } as T & { type: "blob" }; 427 - }, 428 - /** 429 - * Creates an array type with item schema and length constraints. 430 - * @see https://atproto.com/specs/lexicon#array 431 - */ 432 - array<Items extends { type: LexiconType }, Options extends ArrayOptions>( 433 - items: Items, 434 - options?: Options, 435 - ): Options & { type: "array"; items: Items } { 436 - return { 437 - type: "array", 438 - items, 439 - ...options, 440 - } as Options & { type: "array"; items: Items }; 441 - }, 442 - /** 443 - * Creates a token type for symbolic values in unions. 444 - * @see https://atproto.com/specs/lexicon#token 445 - */ 446 - token<Description extends string>( 447 - description: Description, 448 - ): { type: "token"; description: Description } { 449 - return { type: "token", description }; 450 - }, 451 - /** 452 - * Creates a reference to another schema definition. 453 - * @see https://atproto.com/specs/lexicon#ref 454 - */ 455 - ref<Ref extends string>( 456 - ref: Ref, 457 - options?: LexiconItemCommonOptions, 458 - ): LexiconItemCommonOptions & { type: "ref"; ref: Ref } { 459 - return { 460 - type: "ref", 461 - ref, 462 - ...options, 463 - } as LexiconItemCommonOptions & { type: "ref"; ref: Ref }; 464 - }, 465 - /** 466 - * Creates a union type for multiple possible type variants. 467 - * @see https://atproto.com/specs/lexicon#union 468 - */ 469 - union<const Refs extends readonly string[], Options extends UnionOptions>( 470 - refs: Refs, 471 - options?: Options, 472 - ): Options & { type: "union"; refs: Refs } { 473 - return { 474 - type: "union", 475 - refs, 476 - ...options, 477 - } as Options & { type: "union"; refs: Refs }; 478 - }, 479 - /** 480 - * Creates a record type for repository records. 481 - * @see https://atproto.com/specs/lexicon#record 482 - */ 483 - record<T extends RecordOptions>(options: T): T & { type: "record" } { 484 - return { 485 - type: "record", 486 - ...options, 487 - }; 488 - }, 489 - /** 490 - * Creates an object type with defined properties. 491 - * @see https://atproto.com/specs/lexicon#object 492 - */ 493 - object<T extends ObjectProperties>(options: T): ObjectResult<T> { 494 - const required = Object.keys(options).filter( 495 - (key) => "required" in options[key] && options[key].required, 496 - ); 497 - const nullable = Object.keys(options).filter( 498 - (key) => "nullable" in options[key] && options[key].nullable, 499 - ); 500 - const result: Record<string, unknown> = { 501 - type: "object", 502 - properties: options, 503 - }; 504 - if (required.length > 0) { 505 - result.required = required; 506 - } 507 - if (nullable.length > 0) { 508 - result.nullable = nullable; 509 - } 510 - return result as ObjectResult<T>; 511 - }, 512 - /** 513 - * Creates a params type for query string parameters. 514 - * @see https://atproto.com/specs/lexicon#params 515 - */ 516 - params<Properties extends ParamsProperties>( 517 - properties: Properties, 518 - ): ParamsResult<Properties> { 519 - const required = Object.keys(properties).filter( 520 - (key) => properties[key].required, 521 - ); 522 - const result: Record<string, unknown> = { 523 - type: "params", 524 - properties, 525 - }; 526 - if (required.length > 0) { 527 - result.required = required; 528 - } 529 - return result as ParamsResult<Properties>; 530 - }, 531 - /** 532 - * Creates a query endpoint definition (HTTP GET). 533 - * @see https://atproto.com/specs/lexicon#query 534 - */ 535 - query<T extends QueryOptions>(options?: T): T & { type: "query" } { 536 - return { 537 - type: "query", 538 - ...options, 539 - } as T & { type: "query" }; 540 - }, 541 - /** 542 - * Creates a procedure endpoint definition (HTTP POST). 543 - * @see https://atproto.com/specs/lexicon#procedure 544 - */ 545 - procedure<T extends ProcedureOptions>( 546 - options?: T, 547 - ): T & { type: "procedure" } { 548 - return { 549 - type: "procedure", 550 - ...options, 551 - } as T & { type: "procedure" }; 552 - }, 553 - /** 554 - * Creates a subscription endpoint definition (WebSocket). 555 - * @see https://atproto.com/specs/lexicon#subscription 556 - */ 557 - subscription<T extends SubscriptionOptions>( 558 - options?: T, 559 - ): T & { type: "subscription" } { 560 - return { 561 - type: "subscription", 562 - ...options, 563 - } as T & { type: "subscription" }; 564 - }, 565 - /** 566 - * Creates a lexicon namespace document. 567 - * @see https://atproto.com/specs/lexicon#lexicon-document 568 - */ 569 - namespace<ID extends string, D extends LexiconNamespace["defs"]>( 570 - id: ID, 571 - defs: D, 572 - ): Namespace<{ lexicon: 1; id: ID; defs: D }> { 573 - return new Namespace({ 574 - lexicon: 1, 575 - id, 576 - defs, 577 - }); 578 - }, 579 - };
-19
src/type-utils.ts
··· 1 - /** 2 - * Converts a string union type to a tuple type 3 - * @example 4 - * type Colors = "red" | "green" | "blue"; 5 - * type ColorTuple = UnionToTuple<Colors>; // ["red", "green", "blue"] 6 - */ 7 - export type UnionToTuple<T> = ( 8 - (T extends unknown ? (x: () => T) => void : never) extends ( 9 - x: infer I, 10 - ) => void 11 - ? I 12 - : never 13 - ) extends () => infer R 14 - ? [...UnionToTuple<Exclude<T, R>>, R] 15 - : []; 16 - 17 - export type Prettify<T> = { 18 - [K in keyof T]: T[K]; 19 - } & {};
-40
tests/base-case.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("app.bsky.actor.profile", () => { 5 - const profileNamespace = lx.namespace("app.bsky.actor.profile", { 6 - main: lx.record({ 7 - key: "self", 8 - record: lx.object({ 9 - displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }), 10 - description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 11 - }), 12 - }), 13 - }); 14 - 15 - expect(profileNamespace.json).toEqual({ 16 - lexicon: 1, 17 - id: "app.bsky.actor.profile", 18 - defs: { 19 - main: { 20 - type: "record", 21 - key: "self", 22 - record: { 23 - type: "object", 24 - properties: { 25 - displayName: { 26 - type: "string", 27 - maxLength: 64, 28 - maxGraphemes: 64, 29 - }, 30 - description: { 31 - type: "string", 32 - maxLength: 256, 33 - maxGraphemes: 256, 34 - }, 35 - }, 36 - }, 37 - }, 38 - }, 39 - }); 40 - });
-867
tests/bsky-actor.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("app.bsky.actor.defs - profileViewBasic", () => { 5 - const profileViewBasic = lx.object({ 6 - did: lx.string({ required: true, format: "did" }), 7 - handle: lx.string({ required: true, format: "handle" }), 8 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 9 - pronouns: lx.string(), 10 - avatar: lx.string({ format: "uri" }), 11 - associated: lx.ref("#profileAssociated"), 12 - viewer: lx.ref("#viewerState"), 13 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 14 - createdAt: lx.string({ format: "datetime" }), 15 - verification: lx.ref("#verificationState"), 16 - status: lx.ref("#statusView"), 17 - }); 18 - 19 - expect(profileViewBasic).toEqual({ 20 - type: "object", 21 - properties: { 22 - did: { type: "string", required: true, format: "did" }, 23 - handle: { type: "string", required: true, format: "handle" }, 24 - displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 25 - pronouns: { type: "string" }, 26 - avatar: { type: "string", format: "uri" }, 27 - associated: { type: "ref", ref: "#profileAssociated" }, 28 - viewer: { type: "ref", ref: "#viewerState" }, 29 - labels: { 30 - type: "array", 31 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 32 - }, 33 - createdAt: { type: "string", format: "datetime" }, 34 - verification: { type: "ref", ref: "#verificationState" }, 35 - status: { type: "ref", ref: "#statusView" }, 36 - }, 37 - required: ["did", "handle"], 38 - }); 39 - }); 40 - 41 - test("app.bsky.actor.defs - profileView", () => { 42 - const profileView = lx.object({ 43 - did: lx.string({ required: true, format: "did" }), 44 - handle: lx.string({ required: true, format: "handle" }), 45 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 46 - pronouns: lx.string(), 47 - description: lx.string({ maxGraphemes: 256, maxLength: 2560 }), 48 - avatar: lx.string({ format: "uri" }), 49 - associated: lx.ref("#profileAssociated"), 50 - indexedAt: lx.string({ format: "datetime" }), 51 - createdAt: lx.string({ format: "datetime" }), 52 - viewer: lx.ref("#viewerState"), 53 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 54 - verification: lx.ref("#verificationState"), 55 - status: lx.ref("#statusView"), 56 - }); 57 - 58 - expect(profileView).toEqual({ 59 - type: "object", 60 - properties: { 61 - did: { type: "string", required: true, format: "did" }, 62 - handle: { type: "string", required: true, format: "handle" }, 63 - displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 64 - pronouns: { type: "string" }, 65 - description: { type: "string", maxGraphemes: 256, maxLength: 2560 }, 66 - avatar: { type: "string", format: "uri" }, 67 - associated: { type: "ref", ref: "#profileAssociated" }, 68 - indexedAt: { type: "string", format: "datetime" }, 69 - createdAt: { type: "string", format: "datetime" }, 70 - viewer: { type: "ref", ref: "#viewerState" }, 71 - labels: { 72 - type: "array", 73 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 74 - }, 75 - verification: { type: "ref", ref: "#verificationState" }, 76 - status: { type: "ref", ref: "#statusView" }, 77 - }, 78 - required: ["did", "handle"], 79 - }); 80 - }); 81 - 82 - test("app.bsky.actor.defs - profileViewDetailed", () => { 83 - const profileViewDetailed = lx.object({ 84 - did: lx.string({ required: true, format: "did" }), 85 - handle: lx.string({ required: true, format: "handle" }), 86 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 87 - description: lx.string({ maxGraphemes: 256, maxLength: 2560 }), 88 - pronouns: lx.string(), 89 - website: lx.string({ format: "uri" }), 90 - avatar: lx.string({ format: "uri" }), 91 - banner: lx.string({ format: "uri" }), 92 - followersCount: lx.integer(), 93 - followsCount: lx.integer(), 94 - postsCount: lx.integer(), 95 - associated: lx.ref("#profileAssociated"), 96 - joinedViaStarterPack: lx.ref("app.bsky.graph.defs#starterPackViewBasic"), 97 - indexedAt: lx.string({ format: "datetime" }), 98 - createdAt: lx.string({ format: "datetime" }), 99 - viewer: lx.ref("#viewerState"), 100 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 101 - pinnedPost: lx.ref("com.atproto.repo.strongRef"), 102 - verification: lx.ref("#verificationState"), 103 - status: lx.ref("#statusView"), 104 - }); 105 - 106 - expect(profileViewDetailed).toEqual({ 107 - type: "object", 108 - properties: { 109 - did: { type: "string", required: true, format: "did" }, 110 - handle: { type: "string", required: true, format: "handle" }, 111 - displayName: { type: "string", maxGraphemes: 64, maxLength: 640 }, 112 - description: { type: "string", maxGraphemes: 256, maxLength: 2560 }, 113 - pronouns: { type: "string" }, 114 - website: { type: "string", format: "uri" }, 115 - avatar: { type: "string", format: "uri" }, 116 - banner: { type: "string", format: "uri" }, 117 - followersCount: { type: "integer" }, 118 - followsCount: { type: "integer" }, 119 - postsCount: { type: "integer" }, 120 - associated: { type: "ref", ref: "#profileAssociated" }, 121 - joinedViaStarterPack: { 122 - type: "ref", 123 - ref: "app.bsky.graph.defs#starterPackViewBasic", 124 - }, 125 - indexedAt: { type: "string", format: "datetime" }, 126 - createdAt: { type: "string", format: "datetime" }, 127 - viewer: { type: "ref", ref: "#viewerState" }, 128 - labels: { 129 - type: "array", 130 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 131 - }, 132 - pinnedPost: { type: "ref", ref: "com.atproto.repo.strongRef" }, 133 - verification: { type: "ref", ref: "#verificationState" }, 134 - status: { type: "ref", ref: "#statusView" }, 135 - }, 136 - required: ["did", "handle"], 137 - }); 138 - }); 139 - 140 - test("app.bsky.actor.defs - profileAssociated", () => { 141 - const profileAssociated = lx.object({ 142 - lists: lx.integer(), 143 - feedgens: lx.integer(), 144 - starterPacks: lx.integer(), 145 - labeler: lx.boolean(), 146 - chat: lx.ref("#profileAssociatedChat"), 147 - activitySubscription: lx.ref("#profileAssociatedActivitySubscription"), 148 - }); 149 - 150 - expect(profileAssociated).toEqual({ 151 - type: "object", 152 - properties: { 153 - lists: { type: "integer" }, 154 - feedgens: { type: "integer" }, 155 - starterPacks: { type: "integer" }, 156 - labeler: { type: "boolean" }, 157 - chat: { type: "ref", ref: "#profileAssociatedChat" }, 158 - activitySubscription: { 159 - type: "ref", 160 - ref: "#profileAssociatedActivitySubscription", 161 - }, 162 - }, 163 - }); 164 - }); 165 - 166 - test("app.bsky.actor.defs - profileAssociatedChat", () => { 167 - const profileAssociatedChat = lx.object({ 168 - allowIncoming: lx.string({ 169 - required: true, 170 - knownValues: ["all", "none", "following"], 171 - }), 172 - }); 173 - 174 - expect(profileAssociatedChat).toEqual({ 175 - type: "object", 176 - properties: { 177 - allowIncoming: { 178 - type: "string", 179 - required: true, 180 - knownValues: ["all", "none", "following"], 181 - }, 182 - }, 183 - required: ["allowIncoming"], 184 - }); 185 - }); 186 - 187 - test("app.bsky.actor.defs - profileAssociatedActivitySubscription", () => { 188 - const profileAssociatedActivitySubscription = lx.object({ 189 - allowSubscriptions: lx.string({ 190 - required: true, 191 - knownValues: ["followers", "mutuals", "none"], 192 - }), 193 - }); 194 - 195 - expect(profileAssociatedActivitySubscription).toEqual({ 196 - type: "object", 197 - properties: { 198 - allowSubscriptions: { 199 - type: "string", 200 - required: true, 201 - knownValues: ["followers", "mutuals", "none"], 202 - }, 203 - }, 204 - required: ["allowSubscriptions"], 205 - }); 206 - }); 207 - 208 - test("app.bsky.actor.defs - viewerState", () => { 209 - const viewerState = lx.object({ 210 - muted: lx.boolean(), 211 - mutedByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 212 - blockedBy: lx.boolean(), 213 - blocking: lx.string({ format: "at-uri" }), 214 - blockingByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 215 - following: lx.string({ format: "at-uri" }), 216 - followedBy: lx.string({ format: "at-uri" }), 217 - knownFollowers: lx.ref("#knownFollowers"), 218 - activitySubscription: lx.ref( 219 - "app.bsky.notification.defs#activitySubscription", 220 - ), 221 - }); 222 - 223 - expect(viewerState).toEqual({ 224 - type: "object", 225 - properties: { 226 - muted: { type: "boolean" }, 227 - mutedByList: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 228 - blockedBy: { type: "boolean" }, 229 - blocking: { type: "string", format: "at-uri" }, 230 - blockingByList: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 231 - following: { type: "string", format: "at-uri" }, 232 - followedBy: { type: "string", format: "at-uri" }, 233 - knownFollowers: { type: "ref", ref: "#knownFollowers" }, 234 - activitySubscription: { 235 - type: "ref", 236 - ref: "app.bsky.notification.defs#activitySubscription", 237 - }, 238 - }, 239 - }); 240 - }); 241 - 242 - test("app.bsky.actor.defs - knownFollowers", () => { 243 - const knownFollowers = lx.object({ 244 - count: lx.integer({ required: true }), 245 - followers: lx.array(lx.ref("#profileViewBasic"), { 246 - required: true, 247 - minLength: 0, 248 - maxLength: 5, 249 - }), 250 - }); 251 - 252 - expect(knownFollowers).toEqual({ 253 - type: "object", 254 - properties: { 255 - count: { type: "integer", required: true }, 256 - followers: { 257 - type: "array", 258 - items: { type: "ref", ref: "#profileViewBasic" }, 259 - required: true, 260 - minLength: 0, 261 - maxLength: 5, 262 - }, 263 - }, 264 - required: ["count", "followers"], 265 - }); 266 - }); 267 - 268 - test("app.bsky.actor.defs - verificationState", () => { 269 - const verificationState = lx.object({ 270 - verifications: lx.array(lx.ref("#verificationView"), { required: true }), 271 - verifiedStatus: lx.string({ 272 - required: true, 273 - knownValues: ["valid", "invalid", "none"], 274 - }), 275 - trustedVerifierStatus: lx.string({ 276 - required: true, 277 - knownValues: ["valid", "invalid", "none"], 278 - }), 279 - }); 280 - 281 - expect(verificationState).toEqual({ 282 - type: "object", 283 - properties: { 284 - verifications: { 285 - type: "array", 286 - items: { type: "ref", ref: "#verificationView" }, 287 - required: true, 288 - }, 289 - verifiedStatus: { 290 - type: "string", 291 - required: true, 292 - knownValues: ["valid", "invalid", "none"], 293 - }, 294 - trustedVerifierStatus: { 295 - type: "string", 296 - required: true, 297 - knownValues: ["valid", "invalid", "none"], 298 - }, 299 - }, 300 - required: ["verifications", "verifiedStatus", "trustedVerifierStatus"], 301 - }); 302 - }); 303 - 304 - test("app.bsky.actor.defs - verificationView", () => { 305 - const verificationView = lx.object({ 306 - issuer: lx.string({ required: true, format: "did" }), 307 - uri: lx.string({ required: true, format: "at-uri" }), 308 - isValid: lx.boolean({ required: true }), 309 - createdAt: lx.string({ required: true, format: "datetime" }), 310 - }); 311 - 312 - expect(verificationView).toEqual({ 313 - type: "object", 314 - properties: { 315 - issuer: { type: "string", required: true, format: "did" }, 316 - uri: { type: "string", required: true, format: "at-uri" }, 317 - isValid: { type: "boolean", required: true }, 318 - createdAt: { type: "string", required: true, format: "datetime" }, 319 - }, 320 - required: ["issuer", "uri", "isValid", "createdAt"], 321 - }); 322 - }); 323 - 324 - test("app.bsky.actor.defs - preferences", () => { 325 - const preferences = lx.array( 326 - lx.union([ 327 - "#adultContentPref", 328 - "#contentLabelPref", 329 - "#savedFeedsPref", 330 - "#savedFeedsPrefV2", 331 - "#personalDetailsPref", 332 - "#feedViewPref", 333 - "#threadViewPref", 334 - "#interestsPref", 335 - "#mutedWordsPref", 336 - "#hiddenPostsPref", 337 - "#bskyAppStatePref", 338 - "#labelersPref", 339 - "#postInteractionSettingsPref", 340 - "#verificationPrefs", 341 - ]), 342 - ); 343 - 344 - expect(preferences).toEqual({ 345 - type: "array", 346 - items: { 347 - type: "union", 348 - refs: [ 349 - "#adultContentPref", 350 - "#contentLabelPref", 351 - "#savedFeedsPref", 352 - "#savedFeedsPrefV2", 353 - "#personalDetailsPref", 354 - "#feedViewPref", 355 - "#threadViewPref", 356 - "#interestsPref", 357 - "#mutedWordsPref", 358 - "#hiddenPostsPref", 359 - "#bskyAppStatePref", 360 - "#labelersPref", 361 - "#postInteractionSettingsPref", 362 - "#verificationPrefs", 363 - ], 364 - }, 365 - }); 366 - }); 367 - 368 - test("app.bsky.actor.defs - adultContentPref", () => { 369 - const adultContentPref = lx.object({ 370 - enabled: lx.boolean({ required: true, default: false }), 371 - }); 372 - 373 - expect(adultContentPref).toEqual({ 374 - type: "object", 375 - properties: { 376 - enabled: { type: "boolean", required: true, default: false }, 377 - }, 378 - required: ["enabled"], 379 - }); 380 - }); 381 - 382 - test("app.bsky.actor.defs - contentLabelPref", () => { 383 - const contentLabelPref = lx.object({ 384 - labelerDid: lx.string({ format: "did" }), 385 - label: lx.string({ required: true }), 386 - visibility: lx.string({ 387 - required: true, 388 - knownValues: ["ignore", "show", "warn", "hide"], 389 - }), 390 - }); 391 - 392 - expect(contentLabelPref).toEqual({ 393 - type: "object", 394 - properties: { 395 - labelerDid: { type: "string", format: "did" }, 396 - label: { type: "string", required: true }, 397 - visibility: { 398 - type: "string", 399 - required: true, 400 - knownValues: ["ignore", "show", "warn", "hide"], 401 - }, 402 - }, 403 - required: ["label", "visibility"], 404 - }); 405 - }); 406 - 407 - test("app.bsky.actor.defs - savedFeed", () => { 408 - const savedFeed = lx.object({ 409 - id: lx.string({ required: true }), 410 - type: lx.string({ 411 - required: true, 412 - knownValues: ["feed", "list", "timeline"], 413 - }), 414 - value: lx.string({ required: true }), 415 - pinned: lx.boolean({ required: true }), 416 - }); 417 - 418 - expect(savedFeed).toEqual({ 419 - type: "object", 420 - properties: { 421 - id: { type: "string", required: true }, 422 - type: { 423 - type: "string", 424 - required: true, 425 - knownValues: ["feed", "list", "timeline"], 426 - }, 427 - value: { type: "string", required: true }, 428 - pinned: { type: "boolean", required: true }, 429 - }, 430 - required: ["id", "type", "value", "pinned"], 431 - }); 432 - }); 433 - 434 - test("app.bsky.actor.defs - savedFeedsPrefV2", () => { 435 - const savedFeedsPrefV2 = lx.object({ 436 - items: lx.array(lx.ref("app.bsky.actor.defs#savedFeed"), { 437 - required: true, 438 - }), 439 - }); 440 - 441 - expect(savedFeedsPrefV2).toEqual({ 442 - type: "object", 443 - properties: { 444 - items: { 445 - type: "array", 446 - items: { type: "ref", ref: "app.bsky.actor.defs#savedFeed" }, 447 - required: true, 448 - }, 449 - }, 450 - required: ["items"], 451 - }); 452 - }); 453 - 454 - test("app.bsky.actor.defs - savedFeedsPref", () => { 455 - const savedFeedsPref = lx.object({ 456 - pinned: lx.array(lx.string({ format: "at-uri" }), { required: true }), 457 - saved: lx.array(lx.string({ format: "at-uri" }), { required: true }), 458 - timelineIndex: lx.integer(), 459 - }); 460 - 461 - expect(savedFeedsPref).toEqual({ 462 - type: "object", 463 - properties: { 464 - pinned: { 465 - type: "array", 466 - items: { type: "string", format: "at-uri" }, 467 - required: true, 468 - }, 469 - saved: { 470 - type: "array", 471 - items: { type: "string", format: "at-uri" }, 472 - required: true, 473 - }, 474 - timelineIndex: { type: "integer" }, 475 - }, 476 - required: ["pinned", "saved"], 477 - }); 478 - }); 479 - 480 - test("app.bsky.actor.defs - personalDetailsPref", () => { 481 - const personalDetailsPref = lx.object({ 482 - birthDate: lx.string({ format: "datetime" }), 483 - }); 484 - 485 - expect(personalDetailsPref).toEqual({ 486 - type: "object", 487 - properties: { 488 - birthDate: { type: "string", format: "datetime" }, 489 - }, 490 - }); 491 - }); 492 - 493 - test("app.bsky.actor.defs - feedViewPref", () => { 494 - const feedViewPref = lx.object({ 495 - feed: lx.string({ required: true }), 496 - hideReplies: lx.boolean(), 497 - hideRepliesByUnfollowed: lx.boolean({ default: true }), 498 - hideRepliesByLikeCount: lx.integer(), 499 - hideReposts: lx.boolean(), 500 - hideQuotePosts: lx.boolean(), 501 - }); 502 - 503 - expect(feedViewPref).toEqual({ 504 - type: "object", 505 - properties: { 506 - feed: { type: "string", required: true }, 507 - hideReplies: { type: "boolean" }, 508 - hideRepliesByUnfollowed: { type: "boolean", default: true }, 509 - hideRepliesByLikeCount: { type: "integer" }, 510 - hideReposts: { type: "boolean" }, 511 - hideQuotePosts: { type: "boolean" }, 512 - }, 513 - required: ["feed"], 514 - }); 515 - }); 516 - 517 - test("app.bsky.actor.defs - threadViewPref", () => { 518 - const threadViewPref = lx.object({ 519 - sort: lx.string({ 520 - knownValues: ["oldest", "newest", "most-likes", "random", "hotness"], 521 - }), 522 - prioritizeFollowedUsers: lx.boolean(), 523 - }); 524 - 525 - expect(threadViewPref).toEqual({ 526 - type: "object", 527 - properties: { 528 - sort: { 529 - type: "string", 530 - knownValues: ["oldest", "newest", "most-likes", "random", "hotness"], 531 - }, 532 - prioritizeFollowedUsers: { type: "boolean" }, 533 - }, 534 - }); 535 - }); 536 - 537 - test("app.bsky.actor.defs - interestsPref", () => { 538 - const interestsPref = lx.object({ 539 - tags: lx.array(lx.string({ maxLength: 640, maxGraphemes: 64 }), { 540 - required: true, 541 - maxLength: 100, 542 - }), 543 - }); 544 - 545 - expect(interestsPref).toEqual({ 546 - type: "object", 547 - properties: { 548 - tags: { 549 - type: "array", 550 - items: { type: "string", maxLength: 640, maxGraphemes: 64 }, 551 - required: true, 552 - maxLength: 100, 553 - }, 554 - }, 555 - required: ["tags"], 556 - }); 557 - }); 558 - 559 - test("app.bsky.actor.defs - mutedWordTarget", () => { 560 - const mutedWordTarget = lx.string({ 561 - knownValues: ["content", "tag"], 562 - maxLength: 640, 563 - maxGraphemes: 64, 564 - }); 565 - 566 - expect(mutedWordTarget).toEqual({ 567 - type: "string", 568 - knownValues: ["content", "tag"], 569 - maxLength: 640, 570 - maxGraphemes: 64, 571 - }); 572 - }); 573 - 574 - test("app.bsky.actor.defs - mutedWord", () => { 575 - const mutedWord = lx.object({ 576 - id: lx.string(), 577 - value: lx.string({ required: true, maxLength: 10000, maxGraphemes: 1000 }), 578 - targets: lx.array(lx.ref("app.bsky.actor.defs#mutedWordTarget"), { 579 - required: true, 580 - }), 581 - actorTarget: lx.string({ 582 - knownValues: ["all", "exclude-following"], 583 - default: "all", 584 - }), 585 - expiresAt: lx.string({ format: "datetime" }), 586 - }); 587 - 588 - expect(mutedWord).toEqual({ 589 - type: "object", 590 - properties: { 591 - id: { type: "string" }, 592 - value: { 593 - type: "string", 594 - required: true, 595 - maxLength: 10000, 596 - maxGraphemes: 1000, 597 - }, 598 - targets: { 599 - type: "array", 600 - items: { type: "ref", ref: "app.bsky.actor.defs#mutedWordTarget" }, 601 - required: true, 602 - }, 603 - actorTarget: { 604 - type: "string", 605 - knownValues: ["all", "exclude-following"], 606 - default: "all", 607 - }, 608 - expiresAt: { type: "string", format: "datetime" }, 609 - }, 610 - required: ["value", "targets"], 611 - }); 612 - }); 613 - 614 - test("app.bsky.actor.defs - mutedWordsPref", () => { 615 - const mutedWordsPref = lx.object({ 616 - items: lx.array(lx.ref("app.bsky.actor.defs#mutedWord"), { 617 - required: true, 618 - }), 619 - }); 620 - 621 - expect(mutedWordsPref).toEqual({ 622 - type: "object", 623 - properties: { 624 - items: { 625 - type: "array", 626 - items: { type: "ref", ref: "app.bsky.actor.defs#mutedWord" }, 627 - required: true, 628 - }, 629 - }, 630 - required: ["items"], 631 - }); 632 - }); 633 - 634 - test("app.bsky.actor.defs - hiddenPostsPref", () => { 635 - const hiddenPostsPref = lx.object({ 636 - items: lx.array(lx.string({ format: "at-uri" }), { required: true }), 637 - }); 638 - 639 - expect(hiddenPostsPref).toEqual({ 640 - type: "object", 641 - properties: { 642 - items: { 643 - type: "array", 644 - items: { type: "string", format: "at-uri" }, 645 - required: true, 646 - }, 647 - }, 648 - required: ["items"], 649 - }); 650 - }); 651 - 652 - test("app.bsky.actor.defs - labelersPref", () => { 653 - const labelersPref = lx.object({ 654 - labelers: lx.array(lx.ref("#labelerPrefItem"), { required: true }), 655 - }); 656 - 657 - expect(labelersPref).toEqual({ 658 - type: "object", 659 - properties: { 660 - labelers: { 661 - type: "array", 662 - items: { type: "ref", ref: "#labelerPrefItem" }, 663 - required: true, 664 - }, 665 - }, 666 - required: ["labelers"], 667 - }); 668 - }); 669 - 670 - test("app.bsky.actor.defs - labelerPrefItem", () => { 671 - const labelerPrefItem = lx.object({ 672 - did: lx.string({ required: true, format: "did" }), 673 - }); 674 - 675 - expect(labelerPrefItem).toEqual({ 676 - type: "object", 677 - properties: { 678 - did: { type: "string", required: true, format: "did" }, 679 - }, 680 - required: ["did"], 681 - }); 682 - }); 683 - 684 - test("app.bsky.actor.defs - bskyAppStatePref", () => { 685 - const bskyAppStatePref = lx.object({ 686 - activeProgressGuide: lx.ref("#bskyAppProgressGuide"), 687 - queuedNudges: lx.array(lx.string({ maxLength: 100 }), { maxLength: 1000 }), 688 - nuxs: lx.array(lx.ref("app.bsky.actor.defs#nux"), { maxLength: 100 }), 689 - }); 690 - 691 - expect(bskyAppStatePref).toEqual({ 692 - type: "object", 693 - properties: { 694 - activeProgressGuide: { type: "ref", ref: "#bskyAppProgressGuide" }, 695 - queuedNudges: { 696 - type: "array", 697 - items: { type: "string", maxLength: 100 }, 698 - maxLength: 1000, 699 - }, 700 - nuxs: { 701 - type: "array", 702 - items: { type: "ref", ref: "app.bsky.actor.defs#nux" }, 703 - maxLength: 100, 704 - }, 705 - }, 706 - }); 707 - }); 708 - 709 - test("app.bsky.actor.defs - bskyAppProgressGuide", () => { 710 - const bskyAppProgressGuide = lx.object({ 711 - guide: lx.string({ required: true, maxLength: 100 }), 712 - }); 713 - 714 - expect(bskyAppProgressGuide).toEqual({ 715 - type: "object", 716 - properties: { 717 - guide: { type: "string", required: true, maxLength: 100 }, 718 - }, 719 - required: ["guide"], 720 - }); 721 - }); 722 - 723 - test("app.bsky.actor.defs - nux", () => { 724 - const nux = lx.object({ 725 - id: lx.string({ required: true, maxLength: 100 }), 726 - completed: lx.boolean({ required: true, default: false }), 727 - data: lx.string({ maxLength: 3000, maxGraphemes: 300 }), 728 - expiresAt: lx.string({ format: "datetime" }), 729 - }); 730 - 731 - expect(nux).toEqual({ 732 - type: "object", 733 - properties: { 734 - id: { type: "string", required: true, maxLength: 100 }, 735 - completed: { type: "boolean", required: true, default: false }, 736 - data: { type: "string", maxLength: 3000, maxGraphemes: 300 }, 737 - expiresAt: { type: "string", format: "datetime" }, 738 - }, 739 - required: ["id", "completed"], 740 - }); 741 - }); 742 - 743 - test("app.bsky.actor.defs - verificationPrefs", () => { 744 - const verificationPrefs = lx.object({ 745 - hideBadges: lx.boolean({ default: false }), 746 - }); 747 - 748 - expect(verificationPrefs).toEqual({ 749 - type: "object", 750 - properties: { 751 - hideBadges: { type: "boolean", default: false }, 752 - }, 753 - }); 754 - }); 755 - 756 - test("app.bsky.actor.defs - postInteractionSettingsPref", () => { 757 - const postInteractionSettingsPref = lx.object({ 758 - threadgateAllowRules: lx.array( 759 - lx.union([ 760 - "app.bsky.feed.threadgate#mentionRule", 761 - "app.bsky.feed.threadgate#followerRule", 762 - "app.bsky.feed.threadgate#followingRule", 763 - "app.bsky.feed.threadgate#listRule", 764 - ]), 765 - { maxLength: 5 }, 766 - ), 767 - postgateEmbeddingRules: lx.array( 768 - lx.union(["app.bsky.feed.postgate#disableRule"]), 769 - { maxLength: 5 }, 770 - ), 771 - }); 772 - 773 - expect(postInteractionSettingsPref).toEqual({ 774 - type: "object", 775 - properties: { 776 - threadgateAllowRules: { 777 - type: "array", 778 - items: { 779 - type: "union", 780 - refs: [ 781 - "app.bsky.feed.threadgate#mentionRule", 782 - "app.bsky.feed.threadgate#followerRule", 783 - "app.bsky.feed.threadgate#followingRule", 784 - "app.bsky.feed.threadgate#listRule", 785 - ], 786 - }, 787 - maxLength: 5, 788 - }, 789 - postgateEmbeddingRules: { 790 - type: "array", 791 - items: { 792 - type: "union", 793 - refs: ["app.bsky.feed.postgate#disableRule"], 794 - }, 795 - maxLength: 5, 796 - }, 797 - }, 798 - }); 799 - }); 800 - 801 - test("app.bsky.actor.defs - statusView", () => { 802 - const statusView = lx.object({ 803 - status: lx.string({ 804 - required: true, 805 - knownValues: ["app.bsky.actor.status#live"], 806 - }), 807 - record: lx.unknown({ required: true }), 808 - embed: lx.union(["app.bsky.embed.external#view"]), 809 - expiresAt: lx.string({ format: "datetime" }), 810 - isActive: lx.boolean(), 811 - }); 812 - 813 - expect(statusView).toEqual({ 814 - type: "object", 815 - properties: { 816 - status: { 817 - type: "string", 818 - required: true, 819 - knownValues: ["app.bsky.actor.status#live"], 820 - }, 821 - record: { type: "unknown", required: true }, 822 - embed: { 823 - type: "union", 824 - refs: ["app.bsky.embed.external#view"], 825 - }, 826 - expiresAt: { type: "string", format: "datetime" }, 827 - isActive: { type: "boolean" }, 828 - }, 829 - required: ["status", "record"], 830 - }); 831 - }); 832 - 833 - test("app.bsky.actor.defs - full namespace", () => { 834 - const actorDefs = lx.namespace("app.bsky.actor.defs", { 835 - profileViewBasic: lx.object({ 836 - did: lx.string({ required: true, format: "did" }), 837 - handle: lx.string({ required: true, format: "handle" }), 838 - displayName: lx.string({ maxGraphemes: 64, maxLength: 640 }), 839 - pronouns: lx.string(), 840 - avatar: lx.string({ format: "uri" }), 841 - associated: lx.ref("#profileAssociated"), 842 - viewer: lx.ref("#viewerState"), 843 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 844 - createdAt: lx.string({ format: "datetime" }), 845 - verification: lx.ref("#verificationState"), 846 - status: lx.ref("#statusView"), 847 - }), 848 - viewerState: lx.object({ 849 - muted: lx.boolean(), 850 - mutedByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 851 - blockedBy: lx.boolean(), 852 - blocking: lx.string({ format: "at-uri" }), 853 - blockingByList: lx.ref("app.bsky.graph.defs#listViewBasic"), 854 - following: lx.string({ format: "at-uri" }), 855 - followedBy: lx.string({ format: "at-uri" }), 856 - knownFollowers: lx.ref("#knownFollowers"), 857 - activitySubscription: lx.ref( 858 - "app.bsky.notification.defs#activitySubscription", 859 - ), 860 - }), 861 - }); 862 - 863 - expect(actorDefs.json.lexicon).toEqual(1); 864 - expect(actorDefs.json.id).toEqual("app.bsky.actor.defs"); 865 - expect(actorDefs.json.defs.profileViewBasic.type).toEqual("object"); 866 - expect(actorDefs.json.defs.viewerState.type).toEqual("object"); 867 - });
-681
tests/bsky-feed.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("app.bsky.feed.defs - postView", () => { 5 - const postView = lx.object({ 6 - uri: lx.string({ required: true, format: "at-uri" }), 7 - cid: lx.string({ required: true, format: "cid" }), 8 - author: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 9 - record: lx.unknown({ required: true }), 10 - embed: lx.union([ 11 - "app.bsky.embed.images#view", 12 - "app.bsky.embed.video#view", 13 - "app.bsky.embed.external#view", 14 - "app.bsky.embed.record#view", 15 - "app.bsky.embed.recordWithMedia#view", 16 - ]), 17 - bookmarkCount: lx.integer(), 18 - replyCount: lx.integer(), 19 - repostCount: lx.integer(), 20 - likeCount: lx.integer(), 21 - quoteCount: lx.integer(), 22 - indexedAt: lx.string({ required: true, format: "datetime" }), 23 - viewer: lx.ref("#viewerState"), 24 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 25 - threadgate: lx.ref("#threadgateView"), 26 - }); 27 - 28 - expect(postView).toEqual({ 29 - type: "object", 30 - properties: { 31 - uri: { type: "string", required: true, format: "at-uri" }, 32 - cid: { type: "string", required: true, format: "cid" }, 33 - author: { 34 - type: "ref", 35 - ref: "app.bsky.actor.defs#profileViewBasic", 36 - required: true, 37 - }, 38 - record: { type: "unknown", required: true }, 39 - embed: { 40 - type: "union", 41 - refs: [ 42 - "app.bsky.embed.images#view", 43 - "app.bsky.embed.video#view", 44 - "app.bsky.embed.external#view", 45 - "app.bsky.embed.record#view", 46 - "app.bsky.embed.recordWithMedia#view", 47 - ], 48 - }, 49 - bookmarkCount: { type: "integer" }, 50 - replyCount: { type: "integer" }, 51 - repostCount: { type: "integer" }, 52 - likeCount: { type: "integer" }, 53 - quoteCount: { type: "integer" }, 54 - indexedAt: { type: "string", required: true, format: "datetime" }, 55 - viewer: { type: "ref", ref: "#viewerState" }, 56 - labels: { 57 - type: "array", 58 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 59 - }, 60 - threadgate: { type: "ref", ref: "#threadgateView" }, 61 - }, 62 - required: ["uri", "cid", "author", "record", "indexedAt"], 63 - }); 64 - }); 65 - 66 - test("app.bsky.feed.defs - viewerState", () => { 67 - const viewerState = lx.object({ 68 - repost: lx.string({ format: "at-uri" }), 69 - like: lx.string({ format: "at-uri" }), 70 - bookmarked: lx.boolean(), 71 - threadMuted: lx.boolean(), 72 - replyDisabled: lx.boolean(), 73 - embeddingDisabled: lx.boolean(), 74 - pinned: lx.boolean(), 75 - }); 76 - 77 - expect(viewerState).toEqual({ 78 - type: "object", 79 - properties: { 80 - repost: { type: "string", format: "at-uri" }, 81 - like: { type: "string", format: "at-uri" }, 82 - bookmarked: { type: "boolean" }, 83 - threadMuted: { type: "boolean" }, 84 - replyDisabled: { type: "boolean" }, 85 - embeddingDisabled: { type: "boolean" }, 86 - pinned: { type: "boolean" }, 87 - }, 88 - }); 89 - }); 90 - 91 - test("app.bsky.feed.defs - threadContext", () => { 92 - const threadContext = lx.object({ 93 - rootAuthorLike: lx.string({ format: "at-uri" }), 94 - }); 95 - 96 - expect(threadContext).toEqual({ 97 - type: "object", 98 - properties: { 99 - rootAuthorLike: { type: "string", format: "at-uri" }, 100 - }, 101 - }); 102 - }); 103 - 104 - test("app.bsky.feed.defs - feedViewPost", () => { 105 - const feedViewPost = lx.object({ 106 - post: lx.ref("#postView", { required: true }), 107 - reply: lx.ref("#replyRef"), 108 - reason: lx.union(["#reasonRepost", "#reasonPin"]), 109 - feedContext: lx.string({ maxLength: 2000 }), 110 - reqId: lx.string({ maxLength: 100 }), 111 - }); 112 - 113 - expect(feedViewPost).toEqual({ 114 - type: "object", 115 - properties: { 116 - post: { type: "ref", ref: "#postView", required: true }, 117 - reply: { type: "ref", ref: "#replyRef" }, 118 - reason: { 119 - type: "union", 120 - refs: ["#reasonRepost", "#reasonPin"], 121 - }, 122 - feedContext: { type: "string", maxLength: 2000 }, 123 - reqId: { type: "string", maxLength: 100 }, 124 - }, 125 - required: ["post"], 126 - }); 127 - }); 128 - 129 - test("app.bsky.feed.defs - replyRef", () => { 130 - const replyRef = lx.object({ 131 - root: lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 132 - required: true, 133 - }), 134 - parent: lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 135 - required: true, 136 - }), 137 - grandparentAuthor: lx.ref("app.bsky.actor.defs#profileViewBasic"), 138 - }); 139 - 140 - expect(replyRef).toEqual({ 141 - type: "object", 142 - properties: { 143 - root: { 144 - type: "union", 145 - refs: ["#postView", "#notFoundPost", "#blockedPost"], 146 - required: true, 147 - }, 148 - parent: { 149 - type: "union", 150 - refs: ["#postView", "#notFoundPost", "#blockedPost"], 151 - required: true, 152 - }, 153 - grandparentAuthor: { 154 - type: "ref", 155 - ref: "app.bsky.actor.defs#profileViewBasic", 156 - }, 157 - }, 158 - required: ["root", "parent"], 159 - }); 160 - }); 161 - 162 - test("app.bsky.feed.defs - reasonRepost", () => { 163 - const reasonRepost = lx.object({ 164 - by: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }), 165 - uri: lx.string({ format: "at-uri" }), 166 - cid: lx.string({ format: "cid" }), 167 - indexedAt: lx.string({ required: true, format: "datetime" }), 168 - }); 169 - 170 - expect(reasonRepost).toEqual({ 171 - type: "object", 172 - properties: { 173 - by: { 174 - type: "ref", 175 - ref: "app.bsky.actor.defs#profileViewBasic", 176 - required: true, 177 - }, 178 - uri: { type: "string", format: "at-uri" }, 179 - cid: { type: "string", format: "cid" }, 180 - indexedAt: { type: "string", required: true, format: "datetime" }, 181 - }, 182 - required: ["by", "indexedAt"], 183 - }); 184 - }); 185 - 186 - test("app.bsky.feed.defs - reasonPin", () => { 187 - const reasonPin = lx.object({}); 188 - 189 - expect(reasonPin).toEqual({ 190 - type: "object", 191 - properties: {}, 192 - }); 193 - }); 194 - 195 - test("app.bsky.feed.defs - threadViewPost", () => { 196 - const threadViewPost = lx.object({ 197 - post: lx.ref("#postView", { required: true }), 198 - parent: lx.union(["#threadViewPost", "#notFoundPost", "#blockedPost"]), 199 - replies: lx.array( 200 - lx.union(["#threadViewPost", "#notFoundPost", "#blockedPost"]), 201 - ), 202 - threadContext: lx.ref("#threadContext"), 203 - }); 204 - 205 - expect(threadViewPost).toEqual({ 206 - type: "object", 207 - properties: { 208 - post: { type: "ref", ref: "#postView", required: true }, 209 - parent: { 210 - type: "union", 211 - refs: ["#threadViewPost", "#notFoundPost", "#blockedPost"], 212 - }, 213 - replies: { 214 - type: "array", 215 - items: { 216 - type: "union", 217 - refs: ["#threadViewPost", "#notFoundPost", "#blockedPost"], 218 - }, 219 - }, 220 - threadContext: { type: "ref", ref: "#threadContext" }, 221 - }, 222 - required: ["post"], 223 - }); 224 - }); 225 - 226 - test("app.bsky.feed.defs - notFoundPost", () => { 227 - const notFoundPost = lx.object({ 228 - uri: lx.string({ required: true, format: "at-uri" }), 229 - notFound: lx.boolean({ required: true, const: true }), 230 - }); 231 - 232 - expect(notFoundPost).toEqual({ 233 - type: "object", 234 - properties: { 235 - uri: { type: "string", required: true, format: "at-uri" }, 236 - notFound: { type: "boolean", required: true, const: true }, 237 - }, 238 - required: ["uri", "notFound"], 239 - }); 240 - }); 241 - 242 - test("app.bsky.feed.defs - blockedPost", () => { 243 - const blockedPost = lx.object({ 244 - uri: lx.string({ required: true, format: "at-uri" }), 245 - blocked: lx.boolean({ required: true, const: true }), 246 - author: lx.ref("#blockedAuthor", { required: true }), 247 - }); 248 - 249 - expect(blockedPost).toEqual({ 250 - type: "object", 251 - properties: { 252 - uri: { type: "string", required: true, format: "at-uri" }, 253 - blocked: { type: "boolean", required: true, const: true }, 254 - author: { type: "ref", ref: "#blockedAuthor", required: true }, 255 - }, 256 - required: ["uri", "blocked", "author"], 257 - }); 258 - }); 259 - 260 - test("app.bsky.feed.defs - blockedAuthor", () => { 261 - const blockedAuthor = lx.object({ 262 - did: lx.string({ required: true, format: "did" }), 263 - viewer: lx.ref("app.bsky.actor.defs#viewerState"), 264 - }); 265 - 266 - expect(blockedAuthor).toEqual({ 267 - type: "object", 268 - properties: { 269 - did: { type: "string", required: true, format: "did" }, 270 - viewer: { type: "ref", ref: "app.bsky.actor.defs#viewerState" }, 271 - }, 272 - required: ["did"], 273 - }); 274 - }); 275 - 276 - test("app.bsky.feed.defs - generatorView", () => { 277 - const generatorView = lx.object({ 278 - uri: lx.string({ required: true, format: "at-uri" }), 279 - cid: lx.string({ required: true, format: "cid" }), 280 - did: lx.string({ required: true, format: "did" }), 281 - creator: lx.ref("app.bsky.actor.defs#profileView", { required: true }), 282 - displayName: lx.string({ required: true }), 283 - description: lx.string({ maxGraphemes: 300, maxLength: 3000 }), 284 - descriptionFacets: lx.array(lx.ref("app.bsky.richtext.facet")), 285 - avatar: lx.string({ format: "uri" }), 286 - likeCount: lx.integer({ minimum: 0 }), 287 - acceptsInteractions: lx.boolean(), 288 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 289 - viewer: lx.ref("#generatorViewerState"), 290 - contentMode: lx.string({ 291 - knownValues: [ 292 - "app.bsky.feed.defs#contentModeUnspecified", 293 - "app.bsky.feed.defs#contentModeVideo", 294 - ], 295 - }), 296 - indexedAt: lx.string({ required: true, format: "datetime" }), 297 - }); 298 - 299 - expect(generatorView).toEqual({ 300 - type: "object", 301 - properties: { 302 - uri: { type: "string", required: true, format: "at-uri" }, 303 - cid: { type: "string", required: true, format: "cid" }, 304 - did: { type: "string", required: true, format: "did" }, 305 - creator: { 306 - type: "ref", 307 - ref: "app.bsky.actor.defs#profileView", 308 - required: true, 309 - }, 310 - displayName: { type: "string", required: true }, 311 - description: { type: "string", maxGraphemes: 300, maxLength: 3000 }, 312 - descriptionFacets: { 313 - type: "array", 314 - items: { type: "ref", ref: "app.bsky.richtext.facet" }, 315 - }, 316 - avatar: { type: "string", format: "uri" }, 317 - likeCount: { type: "integer", minimum: 0 }, 318 - acceptsInteractions: { type: "boolean" }, 319 - labels: { 320 - type: "array", 321 - items: { type: "ref", ref: "com.atproto.label.defs#label" }, 322 - }, 323 - viewer: { type: "ref", ref: "#generatorViewerState" }, 324 - contentMode: { 325 - type: "string", 326 - knownValues: [ 327 - "app.bsky.feed.defs#contentModeUnspecified", 328 - "app.bsky.feed.defs#contentModeVideo", 329 - ], 330 - }, 331 - indexedAt: { type: "string", required: true, format: "datetime" }, 332 - }, 333 - required: ["uri", "cid", "did", "creator", "displayName", "indexedAt"], 334 - }); 335 - }); 336 - 337 - test("app.bsky.feed.defs - generatorViewerState", () => { 338 - const generatorViewerState = lx.object({ 339 - like: lx.string({ format: "at-uri" }), 340 - }); 341 - 342 - expect(generatorViewerState).toEqual({ 343 - type: "object", 344 - properties: { 345 - like: { type: "string", format: "at-uri" }, 346 - }, 347 - }); 348 - }); 349 - 350 - test("app.bsky.feed.defs - skeletonFeedPost", () => { 351 - const skeletonFeedPost = lx.object({ 352 - post: lx.string({ required: true, format: "at-uri" }), 353 - reason: lx.union(["#skeletonReasonRepost", "#skeletonReasonPin"]), 354 - feedContext: lx.string({ maxLength: 2000 }), 355 - }); 356 - 357 - expect(skeletonFeedPost).toEqual({ 358 - type: "object", 359 - properties: { 360 - post: { type: "string", required: true, format: "at-uri" }, 361 - reason: { 362 - type: "union", 363 - refs: ["#skeletonReasonRepost", "#skeletonReasonPin"], 364 - }, 365 - feedContext: { type: "string", maxLength: 2000 }, 366 - }, 367 - required: ["post"], 368 - }); 369 - }); 370 - 371 - test("app.bsky.feed.defs - skeletonReasonRepost", () => { 372 - const skeletonReasonRepost = lx.object({ 373 - repost: lx.string({ required: true, format: "at-uri" }), 374 - }); 375 - 376 - expect(skeletonReasonRepost).toEqual({ 377 - type: "object", 378 - properties: { 379 - repost: { type: "string", required: true, format: "at-uri" }, 380 - }, 381 - required: ["repost"], 382 - }); 383 - }); 384 - 385 - test("app.bsky.feed.defs - skeletonReasonPin", () => { 386 - const skeletonReasonPin = lx.object({}); 387 - 388 - expect(skeletonReasonPin).toEqual({ 389 - type: "object", 390 - properties: {}, 391 - }); 392 - }); 393 - 394 - test("app.bsky.feed.defs - threadgateView", () => { 395 - const threadgateView = lx.object({ 396 - uri: lx.string({ format: "at-uri" }), 397 - cid: lx.string({ format: "cid" }), 398 - record: lx.unknown(), 399 - lists: lx.array(lx.ref("app.bsky.graph.defs#listViewBasic")), 400 - }); 401 - 402 - expect(threadgateView).toEqual({ 403 - type: "object", 404 - properties: { 405 - uri: { type: "string", format: "at-uri" }, 406 - cid: { type: "string", format: "cid" }, 407 - record: { type: "unknown" }, 408 - lists: { 409 - type: "array", 410 - items: { type: "ref", ref: "app.bsky.graph.defs#listViewBasic" }, 411 - }, 412 - }, 413 - }); 414 - }); 415 - 416 - test("app.bsky.feed.defs - interaction", () => { 417 - const interaction = lx.object({ 418 - item: lx.string({ format: "at-uri" }), 419 - event: lx.string({ 420 - knownValues: [ 421 - "app.bsky.feed.defs#requestLess", 422 - "app.bsky.feed.defs#requestMore", 423 - "app.bsky.feed.defs#clickthroughItem", 424 - "app.bsky.feed.defs#clickthroughAuthor", 425 - "app.bsky.feed.defs#clickthroughReposter", 426 - "app.bsky.feed.defs#clickthroughEmbed", 427 - "app.bsky.feed.defs#interactionSeen", 428 - "app.bsky.feed.defs#interactionLike", 429 - "app.bsky.feed.defs#interactionRepost", 430 - "app.bsky.feed.defs#interactionReply", 431 - "app.bsky.feed.defs#interactionQuote", 432 - "app.bsky.feed.defs#interactionShare", 433 - ], 434 - }), 435 - feedContext: lx.string({ maxLength: 2000 }), 436 - reqId: lx.string({ maxLength: 100 }), 437 - }); 438 - 439 - expect(interaction).toEqual({ 440 - type: "object", 441 - properties: { 442 - item: { type: "string", format: "at-uri" }, 443 - event: { 444 - type: "string", 445 - knownValues: [ 446 - "app.bsky.feed.defs#requestLess", 447 - "app.bsky.feed.defs#requestMore", 448 - "app.bsky.feed.defs#clickthroughItem", 449 - "app.bsky.feed.defs#clickthroughAuthor", 450 - "app.bsky.feed.defs#clickthroughReposter", 451 - "app.bsky.feed.defs#clickthroughEmbed", 452 - "app.bsky.feed.defs#interactionSeen", 453 - "app.bsky.feed.defs#interactionLike", 454 - "app.bsky.feed.defs#interactionRepost", 455 - "app.bsky.feed.defs#interactionReply", 456 - "app.bsky.feed.defs#interactionQuote", 457 - "app.bsky.feed.defs#interactionShare", 458 - ], 459 - }, 460 - feedContext: { type: "string", maxLength: 2000 }, 461 - reqId: { type: "string", maxLength: 100 }, 462 - }, 463 - }); 464 - }); 465 - 466 - test("app.bsky.feed.defs - requestLess token", () => { 467 - const requestLess = lx.token( 468 - "Request that less content like the given feed item be shown in the feed", 469 - ); 470 - 471 - expect(requestLess).toEqual({ 472 - type: "token", 473 - description: 474 - "Request that less content like the given feed item be shown in the feed", 475 - }); 476 - }); 477 - 478 - test("app.bsky.feed.defs - requestMore token", () => { 479 - const requestMore = lx.token( 480 - "Request that more content like the given feed item be shown in the feed", 481 - ); 482 - 483 - expect(requestMore).toEqual({ 484 - type: "token", 485 - description: 486 - "Request that more content like the given feed item be shown in the feed", 487 - }); 488 - }); 489 - 490 - test("app.bsky.feed.defs - clickthroughItem token", () => { 491 - const clickthroughItem = lx.token("User clicked through to the feed item"); 492 - 493 - expect(clickthroughItem).toEqual({ 494 - type: "token", 495 - description: "User clicked through to the feed item", 496 - }); 497 - }); 498 - 499 - test("app.bsky.feed.defs - clickthroughAuthor token", () => { 500 - const clickthroughAuthor = lx.token( 501 - "User clicked through to the author of the feed item", 502 - ); 503 - 504 - expect(clickthroughAuthor).toEqual({ 505 - type: "token", 506 - description: "User clicked through to the author of the feed item", 507 - }); 508 - }); 509 - 510 - test("app.bsky.feed.defs - clickthroughReposter token", () => { 511 - const clickthroughReposter = lx.token( 512 - "User clicked through to the reposter of the feed item", 513 - ); 514 - 515 - expect(clickthroughReposter).toEqual({ 516 - type: "token", 517 - description: "User clicked through to the reposter of the feed item", 518 - }); 519 - }); 520 - 521 - test("app.bsky.feed.defs - clickthroughEmbed token", () => { 522 - const clickthroughEmbed = lx.token( 523 - "User clicked through to the embedded content of the feed item", 524 - ); 525 - 526 - expect(clickthroughEmbed).toEqual({ 527 - type: "token", 528 - description: 529 - "User clicked through to the embedded content of the feed item", 530 - }); 531 - }); 532 - 533 - test("app.bsky.feed.defs - contentModeUnspecified token", () => { 534 - const contentModeUnspecified = lx.token( 535 - "Declares the feed generator returns any types of posts.", 536 - ); 537 - 538 - expect(contentModeUnspecified).toEqual({ 539 - type: "token", 540 - description: "Declares the feed generator returns any types of posts.", 541 - }); 542 - }); 543 - 544 - test("app.bsky.feed.defs - contentModeVideo token", () => { 545 - const contentModeVideo = lx.token( 546 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 547 - ); 548 - 549 - expect(contentModeVideo).toEqual({ 550 - type: "token", 551 - description: 552 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 553 - }); 554 - }); 555 - 556 - test("app.bsky.feed.defs - interactionSeen token", () => { 557 - const interactionSeen = lx.token("Feed item was seen by user"); 558 - 559 - expect(interactionSeen).toEqual({ 560 - type: "token", 561 - description: "Feed item was seen by user", 562 - }); 563 - }); 564 - 565 - test("app.bsky.feed.defs - interactionLike token", () => { 566 - const interactionLike = lx.token("User liked the feed item"); 567 - 568 - expect(interactionLike).toEqual({ 569 - type: "token", 570 - description: "User liked the feed item", 571 - }); 572 - }); 573 - 574 - test("app.bsky.feed.defs - interactionRepost token", () => { 575 - const interactionRepost = lx.token("User reposted the feed item"); 576 - 577 - expect(interactionRepost).toEqual({ 578 - type: "token", 579 - description: "User reposted the feed item", 580 - }); 581 - }); 582 - 583 - test("app.bsky.feed.defs - interactionReply token", () => { 584 - const interactionReply = lx.token("User replied to the feed item"); 585 - 586 - expect(interactionReply).toEqual({ 587 - type: "token", 588 - description: "User replied to the feed item", 589 - }); 590 - }); 591 - 592 - test("app.bsky.feed.defs - interactionQuote token", () => { 593 - const interactionQuote = lx.token("User quoted the feed item"); 594 - 595 - expect(interactionQuote).toEqual({ 596 - type: "token", 597 - description: "User quoted the feed item", 598 - }); 599 - }); 600 - 601 - test("app.bsky.feed.defs - interactionShare token", () => { 602 - const interactionShare = lx.token("User shared the feed item"); 603 - 604 - expect(interactionShare).toEqual({ 605 - type: "token", 606 - description: "User shared the feed item", 607 - }); 608 - }); 609 - 610 - test("app.bsky.feed.defs - full namespace", () => { 611 - const feedDefs = lx.namespace("app.bsky.feed.defs", { 612 - postView: lx.object({ 613 - uri: lx.string({ required: true, format: "at-uri" }), 614 - cid: lx.string({ required: true, format: "cid" }), 615 - author: lx.ref("app.bsky.actor.defs#profileViewBasic", { 616 - required: true, 617 - }), 618 - record: lx.unknown({ required: true }), 619 - embed: lx.union([ 620 - "app.bsky.embed.images#view", 621 - "app.bsky.embed.video#view", 622 - "app.bsky.embed.external#view", 623 - "app.bsky.embed.record#view", 624 - "app.bsky.embed.recordWithMedia#view", 625 - ]), 626 - bookmarkCount: lx.integer(), 627 - replyCount: lx.integer(), 628 - repostCount: lx.integer(), 629 - likeCount: lx.integer(), 630 - quoteCount: lx.integer(), 631 - indexedAt: lx.string({ required: true, format: "datetime" }), 632 - viewer: lx.ref("#viewerState"), 633 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 634 - threadgate: lx.ref("#threadgateView"), 635 - }), 636 - viewerState: lx.object({ 637 - repost: lx.string({ format: "at-uri" }), 638 - like: lx.string({ format: "at-uri" }), 639 - bookmarked: lx.boolean(), 640 - threadMuted: lx.boolean(), 641 - replyDisabled: lx.boolean(), 642 - embeddingDisabled: lx.boolean(), 643 - pinned: lx.boolean(), 644 - }), 645 - requestLess: lx.token( 646 - "Request that less content like the given feed item be shown in the feed", 647 - ), 648 - requestMore: lx.token( 649 - "Request that more content like the given feed item be shown in the feed", 650 - ), 651 - clickthroughItem: lx.token("User clicked through to the feed item"), 652 - clickthroughAuthor: lx.token( 653 - "User clicked through to the author of the feed item", 654 - ), 655 - clickthroughReposter: lx.token( 656 - "User clicked through to the reposter of the feed item", 657 - ), 658 - clickthroughEmbed: lx.token( 659 - "User clicked through to the embedded content of the feed item", 660 - ), 661 - contentModeUnspecified: lx.token( 662 - "Declares the feed generator returns any types of posts.", 663 - ), 664 - contentModeVideo: lx.token( 665 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 666 - ), 667 - interactionSeen: lx.token("Feed item was seen by user"), 668 - interactionLike: lx.token("User liked the feed item"), 669 - interactionRepost: lx.token("User reposted the feed item"), 670 - interactionReply: lx.token("User replied to the feed item"), 671 - interactionQuote: lx.token("User quoted the feed item"), 672 - interactionShare: lx.token("User shared the feed item"), 673 - }); 674 - 675 - expect(feedDefs.json.lexicon).toEqual(1); 676 - expect(feedDefs.json.id).toEqual("app.bsky.feed.defs"); 677 - expect(feedDefs.json.defs.postView.type).toEqual("object"); 678 - expect(feedDefs.json.defs.viewerState.type).toEqual("object"); 679 - expect(feedDefs.json.defs.requestLess.type).toEqual("token"); 680 - expect(feedDefs.json.defs.contentModeVideo.type).toEqual("token"); 681 - });
-119
tests/infer.bench.ts
··· 1 - import { bench } from "@ark/attest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - bench("infer with simple object", () => { 5 - const schema = lx.namespace("test.simple", { 6 - main: lx.object({ 7 - id: lx.string({ required: true }), 8 - name: lx.string({ required: true }), 9 - }), 10 - }); 11 - return schema.infer; 12 - }).types([899, "instantiations"]); 13 - 14 - bench("infer with complex nested structure", () => { 15 - const schema = lx.namespace("test.complex", { 16 - user: lx.object({ 17 - handle: lx.string({ required: true }), 18 - displayName: lx.string(), 19 - }), 20 - reply: lx.object({ 21 - text: lx.string({ required: true }), 22 - author: lx.ref("#user", { required: true }), 23 - }), 24 - main: lx.record({ 25 - key: "tid", 26 - record: lx.object({ 27 - author: lx.ref("#user", { required: true }), 28 - replies: lx.array(lx.ref("#reply")), 29 - content: lx.string({ required: true }), 30 - createdAt: lx.string({ required: true, format: "datetime" }), 31 - }), 32 - }), 33 - }); 34 - return schema.infer; 35 - }).types([1040, "instantiations"]); 36 - 37 - bench("infer with circular reference", () => { 38 - const ns = lx.namespace("test", { 39 - user: lx.object({ 40 - name: lx.string({ required: true }), 41 - posts: lx.array(lx.ref("#post")), 42 - }), 43 - post: lx.object({ 44 - title: lx.string({ required: true }), 45 - author: lx.ref("#user", { required: true }), 46 - }), 47 - main: lx.object({ 48 - users: lx.array(lx.ref("#user")), 49 - }), 50 - }); 51 - return ns.infer; 52 - }).types([692, "instantiations"]); 53 - 54 - bench("infer with app.bsky.feed.defs namespace", () => { 55 - const schema = lx.namespace("app.bsky.feed.defs", { 56 - viewerState: lx.object({ 57 - repost: lx.string({ format: "at-uri" }), 58 - like: lx.string({ format: "at-uri" }), 59 - bookmarked: lx.boolean(), 60 - threadMuted: lx.boolean(), 61 - replyDisabled: lx.boolean(), 62 - embeddingDisabled: lx.boolean(), 63 - pinned: lx.boolean(), 64 - }), 65 - main: lx.object({ 66 - uri: lx.string({ required: true, format: "at-uri" }), 67 - cid: lx.string({ required: true, format: "cid" }), 68 - author: lx.ref("app.bsky.actor.defs#profileViewBasic", { 69 - required: true, 70 - }), 71 - record: lx.unknown({ required: true }), 72 - embed: lx.union([ 73 - "app.bsky.embed.images#view", 74 - "app.bsky.embed.video#view", 75 - "app.bsky.embed.external#view", 76 - "app.bsky.embed.record#view", 77 - "app.bsky.embed.recordWithMedia#view", 78 - ]), 79 - bookmarkCount: lx.integer(), 80 - replyCount: lx.integer(), 81 - repostCount: lx.integer(), 82 - likeCount: lx.integer(), 83 - quoteCount: lx.integer(), 84 - indexedAt: lx.string({ required: true, format: "datetime" }), 85 - viewer: lx.ref("#viewerState"), 86 - labels: lx.array(lx.ref("com.atproto.label.defs#label")), 87 - threadgate: lx.ref("#threadgateView"), 88 - }), 89 - requestLess: lx.token( 90 - "Request that less content like the given feed item be shown in the feed", 91 - ), 92 - requestMore: lx.token( 93 - "Request that more content like the given feed item be shown in the feed", 94 - ), 95 - clickthroughItem: lx.token("User clicked through to the feed item"), 96 - clickthroughAuthor: lx.token( 97 - "User clicked through to the author of the feed item", 98 - ), 99 - clickthroughReposter: lx.token( 100 - "User clicked through to the reposter of the feed item", 101 - ), 102 - clickthroughEmbed: lx.token( 103 - "User clicked through to the embedded content of the feed item", 104 - ), 105 - contentModeUnspecified: lx.token( 106 - "Declares the feed generator returns any types of posts.", 107 - ), 108 - contentModeVideo: lx.token( 109 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds.", 110 - ), 111 - interactionSeen: lx.token("Feed item was seen by user"), 112 - interactionLike: lx.token("User liked the feed item"), 113 - interactionRepost: lx.token("User reposted the feed item"), 114 - interactionReply: lx.token("User replied to the feed item"), 115 - interactionQuote: lx.token("User quoted the feed item"), 116 - interactionShare: lx.token("User shared the feed item"), 117 - }); 118 - return schema.infer; 119 - }).types([1285, "instantiations"]);
-868
tests/infer.test.ts
··· 1 - import { test } from "vitest"; 2 - import { attest } from "@ark/attest"; 3 - import { lx } from "../src/lib.ts"; 4 - 5 - test("InferNS produces expected type shape", () => { 6 - const exampleLexicon = lx.namespace("com.example.post", { 7 - main: lx.record({ 8 - key: "tid", 9 - record: lx.object({ 10 - text: lx.string({ required: true }), 11 - createdAt: lx.string({ required: true, format: "datetime" }), 12 - likes: lx.integer(), 13 - tags: lx.array(lx.string(), { maxLength: 5 }), 14 - }), 15 - }), 16 - }); 17 - 18 - // Type snapshot - this captures how types appear on hover 19 - attest(exampleLexicon.infer).type.toString.snap(`{ 20 - $type: "com.example.post" 21 - tags?: string[] | undefined 22 - likes?: number | undefined 23 - createdAt: string 24 - text: string 25 - }`); 26 - }); 27 - 28 - test("InferObject handles required fields", () => { 29 - const schema = lx.namespace("test", { 30 - main: lx.object({ 31 - required: lx.string({ required: true }), 32 - optional: lx.string(), 33 - }), 34 - }); 35 - 36 - attest(schema.infer).type.toString.snap(`{ 37 - $type: "test" 38 - optional?: string | undefined 39 - required: string 40 - }`); 41 - }); 42 - 43 - test("InferObject handles nullable fields", () => { 44 - const schema = lx.namespace("test", { 45 - main: lx.object({ 46 - nullable: lx.string({ nullable: true, required: true }), 47 - }), 48 - }); 49 - 50 - attest(schema.infer).type.toString.snap( 51 - '{ $type: "test"; nullable: string | null }', 52 - ); 53 - }); 54 - 55 - // ============================================================================ 56 - // PRIMITIVE TYPES TESTS 57 - // ============================================================================ 58 - 59 - test("InferType handles string primitive", () => { 60 - const namespace = lx.namespace("test.string", { 61 - main: lx.object({ 62 - simpleString: lx.string(), 63 - }), 64 - }); 65 - 66 - attest(namespace.infer).type.toString.snap(`{ 67 - $type: "test.string" 68 - simpleString?: string | undefined 69 - }`); 70 - }); 71 - 72 - test("InferType handles integer primitive", () => { 73 - const namespace = lx.namespace("test.integer", { 74 - main: lx.object({ 75 - count: lx.integer(), 76 - age: lx.integer({ minimum: 0, maximum: 120 }), 77 - }), 78 - }); 79 - 80 - attest(namespace.infer).type.toString.snap(`{ 81 - $type: "test.integer" 82 - count?: number | undefined 83 - age?: number | undefined 84 - }`); 85 - }); 86 - 87 - test("InferType handles boolean primitive", () => { 88 - const namespace = lx.namespace("test.boolean", { 89 - main: lx.object({ 90 - isActive: lx.boolean(), 91 - hasAccess: lx.boolean({ required: true }), 92 - }), 93 - }); 94 - 95 - attest(namespace.infer).type.toString.snap(`{ 96 - $type: "test.boolean" 97 - isActive?: boolean | undefined 98 - hasAccess: boolean 99 - }`); 100 - }); 101 - 102 - test("InferType handles null primitive", () => { 103 - const namespace = lx.namespace("test.null", { 104 - main: lx.object({ 105 - nullValue: lx.null(), 106 - }), 107 - }); 108 - 109 - attest(namespace.infer).type.toString.snap(`{ 110 - $type: "test.null" 111 - nullValue?: null | undefined 112 - }`); 113 - }); 114 - 115 - test("InferType handles unknown primitive", () => { 116 - const namespace = lx.namespace("test.unknown", { 117 - main: lx.object({ 118 - metadata: lx.unknown(), 119 - }), 120 - }); 121 - 122 - attest(namespace.infer).type.toString.snap( 123 - '{ $type: "test.unknown"; metadata?: unknown }', 124 - ); 125 - }); 126 - 127 - test("InferType handles bytes primitive", () => { 128 - const namespace = lx.namespace("test.bytes", { 129 - main: lx.object({ 130 - data: lx.bytes(), 131 - }), 132 - }); 133 - 134 - attest(namespace.infer).type.toString.snap(`{ 135 - $type: "test.bytes" 136 - data?: Uint8Array<ArrayBufferLike> | undefined 137 - }`); 138 - }); 139 - 140 - test("InferType handles blob primitive", () => { 141 - const namespace = lx.namespace("test.blob", { 142 - main: lx.object({ 143 - image: lx.blob({ accept: ["image/png", "image/jpeg"] }), 144 - }), 145 - }); 146 - 147 - attest(namespace.infer).type.toString.snap( 148 - '{ $type: "test.blob"; image?: Blob | undefined }', 149 - ); 150 - }); 151 - 152 - // ============================================================================ 153 - // TOKEN TYPE TESTS 154 - // ============================================================================ 155 - 156 - test("InferToken handles basic token without enum", () => { 157 - const namespace = lx.namespace("test.token", { 158 - main: lx.object({ 159 - symbol: lx.token("A symbolic value"), 160 - }), 161 - }); 162 - 163 - attest(namespace.infer).type.toString.snap(`{ 164 - $type: "test.token" 165 - symbol?: string | undefined 166 - }`); 167 - }); 168 - 169 - // ============================================================================ 170 - // ARRAY TYPE TESTS 171 - // ============================================================================ 172 - 173 - test("InferArray handles string arrays", () => { 174 - const namespace = lx.namespace("test.array.string", { 175 - main: lx.object({ 176 - tags: lx.array(lx.string()), 177 - }), 178 - }); 179 - 180 - attest(namespace.infer).type.toString.snap(`{ 181 - $type: "test.array.string" 182 - tags?: string[] | undefined 183 - }`); 184 - }); 185 - 186 - test("InferArray handles integer arrays", () => { 187 - const namespace = lx.namespace("test.array.integer", { 188 - main: lx.object({ 189 - scores: lx.array(lx.integer(), { minLength: 1, maxLength: 10 }), 190 - }), 191 - }); 192 - 193 - attest(namespace.infer).type.toString.snap(`{ 194 - $type: "test.array.integer" 195 - scores?: number[] | undefined 196 - }`); 197 - }); 198 - 199 - test("InferArray handles boolean arrays", () => { 200 - const namespace = lx.namespace("test.array.boolean", { 201 - main: lx.object({ 202 - flags: lx.array(lx.boolean()), 203 - }), 204 - }); 205 - 206 - attest(namespace.infer).type.toString.snap(`{ 207 - $type: "test.array.boolean" 208 - flags?: boolean[] | undefined 209 - }`); 210 - }); 211 - 212 - test("InferArray handles unknown arrays", () => { 213 - const namespace = lx.namespace("test.array.unknown", { 214 - main: lx.object({ 215 - items: lx.array(lx.unknown()), 216 - }), 217 - }); 218 - 219 - attest(namespace.infer).type.toString.snap(`{ 220 - $type: "test.array.unknown" 221 - items?: unknown[] | undefined 222 - }`); 223 - }); 224 - 225 - // ============================================================================ 226 - // OBJECT PROPERTY COMBINATIONS 227 - // ============================================================================ 228 - 229 - test("InferObject handles mixed optional and required fields", () => { 230 - const namespace = lx.namespace("test.mixed", { 231 - main: lx.object({ 232 - id: lx.string({ required: true }), 233 - name: lx.string({ required: true }), 234 - email: lx.string(), 235 - age: lx.integer(), 236 - }), 237 - }); 238 - 239 - attest(namespace.infer).type.toString.snap(`{ 240 - $type: "test.mixed" 241 - age?: number | undefined 242 - email?: string | undefined 243 - id: string 244 - name: string 245 - }`); 246 - }); 247 - 248 - test("InferObject handles all optional fields", () => { 249 - const namespace = lx.namespace("test.allOptional", { 250 - main: lx.object({ 251 - field1: lx.string(), 252 - field2: lx.integer(), 253 - field3: lx.boolean(), 254 - }), 255 - }); 256 - 257 - attest(namespace.infer).type.toString.snap(`{ 258 - $type: "test.allOptional" 259 - field1?: string | undefined 260 - field2?: number | undefined 261 - field3?: boolean | undefined 262 - }`); 263 - }); 264 - 265 - test("InferObject handles all required fields", () => { 266 - const namespace = lx.namespace("test.allRequired", { 267 - main: lx.object({ 268 - field1: lx.string({ required: true }), 269 - field2: lx.integer({ required: true }), 270 - field3: lx.boolean({ required: true }), 271 - }), 272 - }); 273 - 274 - attest(namespace.infer).type.toString.snap(`{ 275 - $type: "test.allRequired" 276 - field1: string 277 - field2: number 278 - field3: boolean 279 - }`); 280 - }); 281 - 282 - // ============================================================================ 283 - // NULLABLE FIELDS TESTS 284 - // ============================================================================ 285 - 286 - test("InferObject handles nullable optional field", () => { 287 - const namespace = lx.namespace("test.nullableOptional", { 288 - main: lx.object({ 289 - description: lx.string({ nullable: true }), 290 - }), 291 - }); 292 - 293 - attest(namespace.infer).type.toString.snap(`{ 294 - $type: "test.nullableOptional" 295 - description?: string | null | undefined 296 - }`); 297 - }); 298 - 299 - test("InferObject handles multiple nullable fields", () => { 300 - const namespace = lx.namespace("test.multipleNullable", { 301 - main: lx.object({ 302 - field1: lx.string({ nullable: true }), 303 - field2: lx.integer({ nullable: true }), 304 - field3: lx.boolean({ nullable: true }), 305 - }), 306 - }); 307 - 308 - attest(namespace.infer).type.toString.snap(`{ 309 - $type: "test.multipleNullable" 310 - field1?: string | null | undefined 311 - field2?: number | null | undefined 312 - field3?: boolean | null | undefined 313 - }`); 314 - }); 315 - 316 - test("InferObject handles nullable and required field", () => { 317 - const namespace = lx.namespace("test.nullableRequired", { 318 - main: lx.object({ 319 - value: lx.string({ nullable: true, required: true }), 320 - }), 321 - }); 322 - 323 - attest(namespace.infer).type.toString.snap(`{ 324 - $type: "test.nullableRequired" 325 - value: string | null 326 - }`); 327 - }); 328 - 329 - test("InferObject handles mixed nullable, required, and optional", () => { 330 - const namespace = lx.namespace("test.mixedNullable", { 331 - main: lx.object({ 332 - requiredNullable: lx.string({ required: true, nullable: true }), 333 - optionalNullable: lx.string({ nullable: true }), 334 - required: lx.string({ required: true }), 335 - optional: lx.string(), 336 - }), 337 - }); 338 - 339 - attest(namespace.infer).type.toString.snap(`{ 340 - $type: "test.mixedNullable" 341 - optional?: string | undefined 342 - required: string 343 - optionalNullable?: string | null | undefined 344 - requiredNullable: string | null 345 - }`); 346 - }); 347 - 348 - // ============================================================================ 349 - // REF TYPE TESTS 350 - // ============================================================================ 351 - 352 - test("InferRef handles basic reference", () => { 353 - const namespace = lx.namespace("test.ref", { 354 - main: lx.object({ 355 - post: lx.ref("com.example.post"), 356 - }), 357 - }); 358 - 359 - attest(namespace.infer).type.toString.snap(`{ 360 - $type: "test.ref" 361 - post?: 362 - | { [x: string]: unknown; $type: "com.example.post" } 363 - | undefined 364 - }`); 365 - }); 366 - 367 - test("InferRef handles required reference", () => { 368 - const namespace = lx.namespace("test.refRequired", { 369 - main: lx.object({ 370 - author: lx.ref("com.example.user", { required: true }), 371 - }), 372 - }); 373 - 374 - attest(namespace.infer).type.toString.snap(`{ 375 - $type: "test.refRequired" 376 - author?: 377 - | { [x: string]: unknown; $type: "com.example.user" } 378 - | undefined 379 - }`); 380 - }); 381 - 382 - test("InferRef handles nullable reference", () => { 383 - const namespace = lx.namespace("test.refNullable", { 384 - main: lx.object({ 385 - parent: lx.ref("com.example.node", { nullable: true }), 386 - }), 387 - }); 388 - 389 - attest(namespace.infer).type.toString.snap(`{ 390 - $type: "test.refNullable" 391 - parent?: 392 - | { [x: string]: unknown; $type: "com.example.node" } 393 - | undefined 394 - }`); 395 - }); 396 - 397 - // ============================================================================ 398 - // UNION TYPE TESTS 399 - // ============================================================================ 400 - 401 - test("InferUnion handles basic union", () => { 402 - const namespace = lx.namespace("test.union", { 403 - main: lx.object({ 404 - content: lx.union(["com.example.text", "com.example.image"]), 405 - }), 406 - }); 407 - 408 - attest(namespace.infer).type.toString.snap(`{ 409 - $type: "test.union" 410 - content?: 411 - | { [x: string]: unknown; $type: "com.example.text" } 412 - | { [x: string]: unknown; $type: "com.example.image" } 413 - | undefined 414 - }`); 415 - }); 416 - 417 - test("InferUnion handles required union", () => { 418 - const namespace = lx.namespace("test.unionRequired", { 419 - main: lx.object({ 420 - media: lx.union(["com.example.video", "com.example.audio"], { 421 - required: true, 422 - }), 423 - }), 424 - }); 425 - 426 - attest(namespace.infer).type.toString.snap(`{ 427 - $type: "test.unionRequired" 428 - media: 429 - | { [x: string]: unknown; $type: "com.example.video" } 430 - | { [x: string]: unknown; $type: "com.example.audio" } 431 - }`); 432 - }); 433 - 434 - test("InferUnion handles union with many types", () => { 435 - const namespace = lx.namespace("test.unionMultiple", { 436 - main: lx.object({ 437 - attachment: lx.union([ 438 - "com.example.image", 439 - "com.example.video", 440 - "com.example.audio", 441 - "com.example.document", 442 - ]), 443 - }), 444 - }); 445 - 446 - attest(namespace.infer).type.toString.snap(`{ 447 - $type: "test.unionMultiple" 448 - attachment?: 449 - | { [x: string]: unknown; $type: "com.example.image" } 450 - | { [x: string]: unknown; $type: "com.example.video" } 451 - | { [x: string]: unknown; $type: "com.example.audio" } 452 - | { 453 - [x: string]: unknown 454 - $type: "com.example.document" 455 - } 456 - | undefined 457 - }`); 458 - }); 459 - 460 - // ============================================================================ 461 - // PARAMS TYPE TESTS 462 - // ============================================================================ 463 - 464 - test("InferParams handles basic params", () => { 465 - const namespace = lx.namespace("test.params", { 466 - main: lx.params({ 467 - limit: lx.integer(), 468 - offset: lx.integer(), 469 - }), 470 - }); 471 - 472 - attest(namespace.infer).type.toString.snap(`{ 473 - $type: "test.params" 474 - limit?: number | undefined 475 - offset?: number | undefined 476 - }`); 477 - }); 478 - 479 - test("InferParams handles required params", () => { 480 - const namespace = lx.namespace("test.paramsRequired", { 481 - main: lx.params({ 482 - query: lx.string({ required: true }), 483 - limit: lx.integer(), 484 - }), 485 - }); 486 - 487 - attest(namespace.infer).type.toString.snap(`{ 488 - $type: "test.paramsRequired" 489 - limit?: number | undefined 490 - query: string 491 - }`); 492 - }); 493 - 494 - // ============================================================================ 495 - // RECORD TYPE TESTS 496 - // ============================================================================ 497 - 498 - test("InferRecord handles record with object schema", () => { 499 - const namespace = lx.namespace("test.record", { 500 - main: lx.record({ 501 - key: "tid", 502 - record: lx.object({ 503 - title: lx.string({ required: true }), 504 - content: lx.string({ required: true }), 505 - published: lx.boolean(), 506 - }), 507 - }), 508 - }); 509 - 510 - attest(namespace.infer).type.toString.snap(`{ 511 - $type: "test.record" 512 - published?: boolean | undefined 513 - content: string 514 - title: string 515 - }`); 516 - }); 517 - 518 - // ============================================================================ 519 - // NESTED OBJECTS TESTS 520 - // ============================================================================ 521 - 522 - test("InferObject handles nested objects", () => { 523 - const namespace = lx.namespace("test.nested", { 524 - main: lx.object({ 525 - user: lx.object({ 526 - name: lx.string({ required: true }), 527 - email: lx.string({ required: true }), 528 - }), 529 - }), 530 - }); 531 - 532 - attest(namespace.infer).type.toString.snap(`{ 533 - $type: "test.nested" 534 - user?: { name: string; email: string } | undefined 535 - }`); 536 - }); 537 - 538 - test("InferObject handles deeply nested objects", () => { 539 - const namespace = lx.namespace("test.deepNested", { 540 - main: lx.object({ 541 - data: lx.object({ 542 - user: lx.object({ 543 - profile: lx.object({ 544 - name: lx.string({ required: true }), 545 - }), 546 - }), 547 - }), 548 - }), 549 - }); 550 - 551 - attest(namespace.infer).type.toString.snap(`{ 552 - $type: "test.deepNested" 553 - data?: 554 - | { 555 - user?: 556 - | { profile?: { name: string } | undefined } 557 - | undefined 558 - } 559 - | undefined 560 - }`); 561 - }); 562 - 563 - // ============================================================================ 564 - // NESTED ARRAYS TESTS 565 - // ============================================================================ 566 - 567 - test("InferArray handles arrays of objects", () => { 568 - const namespace = lx.namespace("test.arrayOfObjects", { 569 - main: lx.object({ 570 - users: lx.array( 571 - lx.object({ 572 - id: lx.string({ required: true }), 573 - name: lx.string({ required: true }), 574 - }), 575 - ), 576 - }), 577 - }); 578 - 579 - attest(namespace.infer).type.toString.snap(`{ 580 - $type: "test.arrayOfObjects" 581 - users?: { id: string; name: string }[] | undefined 582 - }`); 583 - }); 584 - 585 - test("InferArray handles arrays of arrays", () => { 586 - const schema = lx.object({ 587 - matrix: lx.array(lx.array(lx.integer())), 588 - }); 589 - 590 - const namespace = lx.namespace("test.nestedArrays", { 591 - main: schema, 592 - }); 593 - 594 - attest(namespace.infer).type.toString.snap(`{ 595 - $type: "test.nestedArrays" 596 - matrix?: number[][] | undefined 597 - }`); 598 - }); 599 - 600 - test("InferArray handles arrays of refs", () => { 601 - const namespace = lx.namespace("test.arrayOfRefs", { 602 - main: lx.object({ 603 - followers: lx.array(lx.ref("com.example.user")), 604 - }), 605 - }); 606 - 607 - attest(namespace.infer).type.toString.snap(`{ 608 - $type: "test.arrayOfRefs" 609 - followers?: 610 - | { [x: string]: unknown; $type: "com.example.user" }[] 611 - | undefined 612 - }`); 613 - }); 614 - 615 - // ============================================================================ 616 - // COMPLEX NESTED STRUCTURES 617 - // ============================================================================ 618 - 619 - test("InferObject handles complex nested structure", () => { 620 - const namespace = lx.namespace("test.complex", { 621 - main: lx.object({ 622 - id: lx.string({ required: true }), 623 - author: lx.object({ 624 - did: lx.string({ required: true, format: "did" }), 625 - handle: lx.string({ required: true, format: "handle" }), 626 - avatar: lx.string(), 627 - }), 628 - content: lx.union(["com.example.text", "com.example.image"]), 629 - tags: lx.array(lx.string(), { maxLength: 10 }), 630 - metadata: lx.object({ 631 - views: lx.integer(), 632 - likes: lx.integer(), 633 - shares: lx.integer(), 634 - }), 635 - }), 636 - }); 637 - 638 - attest(namespace.infer).type.toString.snap(`{ 639 - $type: "test.complex" 640 - tags?: string[] | undefined 641 - content?: 642 - | { [x: string]: unknown; $type: "com.example.text" } 643 - | { [x: string]: unknown; $type: "com.example.image" } 644 - | undefined 645 - author?: 646 - | { 647 - avatar?: string | undefined 648 - did: string 649 - handle: string 650 - } 651 - | undefined 652 - metadata?: 653 - | { 654 - likes?: number | undefined 655 - views?: number | undefined 656 - shares?: number | undefined 657 - } 658 - | undefined 659 - id: string 660 - }`); 661 - }); 662 - 663 - // ============================================================================ 664 - // MULTIPLE DEFS IN NAMESPACE 665 - // ============================================================================ 666 - 667 - test("InferNS handles multiple defs in namespace", () => { 668 - const namespace = lx.namespace("com.example.app", { 669 - user: lx.object({ 670 - name: lx.string({ required: true }), 671 - email: lx.string({ required: true }), 672 - }), 673 - post: lx.object({ 674 - title: lx.string({ required: true }), 675 - content: lx.string({ required: true }), 676 - }), 677 - comment: lx.object({ 678 - text: lx.string({ required: true }), 679 - author: lx.ref("com.example.user"), 680 - }), 681 - }); 682 - 683 - attest(namespace.infer).type.toString.snap("never"); 684 - }); 685 - 686 - test("InferNS handles namespace with record and object defs", () => { 687 - const namespace = lx.namespace("com.example.blog", { 688 - main: lx.record({ 689 - key: "tid", 690 - record: lx.object({ 691 - title: lx.string({ required: true }), 692 - body: lx.string({ required: true }), 693 - }), 694 - }), 695 - metadata: lx.object({ 696 - category: lx.string(), 697 - tags: lx.array(lx.string()), 698 - }), 699 - }); 700 - 701 - attest(namespace.infer).type.toString.snap(`{ 702 - $type: "com.example.blog" 703 - title: string 704 - body: string 705 - }`); 706 - }); 707 - 708 - // ============================================================================ 709 - // LOCAL REF RESOLUTION TESTS 710 - // ============================================================================ 711 - 712 - test("Local ref resolution: resolves refs to actual types", () => { 713 - const ns = lx.namespace("test", { 714 - user: lx.object({ 715 - name: lx.string({ required: true }), 716 - email: lx.string({ required: true }), 717 - }), 718 - main: lx.object({ 719 - author: lx.ref("#user", { required: true }), 720 - content: lx.string({ required: true }), 721 - }), 722 - }); 723 - 724 - attest(ns.infer).type.toString.snap(`{ 725 - $type: "test" 726 - author?: 727 - | { name: string; email: string; $type: "#user" } 728 - | undefined 729 - content: string 730 - }`); 731 - }); 732 - 733 - test("Local ref resolution: refs in arrays", () => { 734 - const ns = lx.namespace("test", { 735 - user: lx.object({ 736 - name: lx.string({ required: true }), 737 - }), 738 - main: lx.object({ 739 - users: lx.array(lx.ref("#user")), 740 - }), 741 - }); 742 - 743 - attest(ns.infer).type.toString.snap(`{ 744 - $type: "test" 745 - users?: { name: string; $type: "#user" }[] | undefined 746 - }`); 747 - }); 748 - 749 - test("Local ref resolution: refs in unions", () => { 750 - const ns = lx.namespace("test", { 751 - text: lx.object({ content: lx.string({ required: true }) }), 752 - image: lx.object({ url: lx.string({ required: true }) }), 753 - main: lx.object({ 754 - embed: lx.union(["#text", "#image"]), 755 - }), 756 - }); 757 - 758 - attest(ns.infer).type.toString.snap(`{ 759 - $type: "test" 760 - embed?: 761 - | { content: string; $type: "#text" } 762 - | { url: string; $type: "#image" } 763 - | undefined 764 - }`); 765 - }); 766 - 767 - test("Local ref resolution: nested refs", () => { 768 - const ns = lx.namespace("test", { 769 - profile: lx.object({ 770 - bio: lx.string({ required: true }), 771 - }), 772 - user: lx.object({ 773 - name: lx.string({ required: true }), 774 - profile: lx.ref("#profile", { required: true }), 775 - }), 776 - main: lx.object({ 777 - author: lx.ref("#user", { required: true }), 778 - }), 779 - }); 780 - 781 - attest(ns.infer).type.toString.snap(`{ 782 - $type: "test" 783 - author?: 784 - | { 785 - profile?: 786 - | { bio: string; $type: "#profile" } 787 - | undefined 788 - name: string 789 - $type: "#user" 790 - } 791 - | undefined 792 - }`); 793 - }); 794 - 795 - // ============================================================================ 796 - // EDGE CASE TESTS 797 - // ============================================================================ 798 - 799 - test("Edge case: circular reference detection", () => { 800 - const ns = lx.namespace("test", { 801 - main: lx.object({ 802 - value: lx.string({ required: true }), 803 - parent: lx.ref("#main"), 804 - }), 805 - }); 806 - 807 - attest(ns.infer).type.toString.snap(`{ 808 - $type: "test" 809 - parent?: 810 - | { 811 - parent?: 812 - | "[Circular reference detected: #main]" 813 - | undefined 814 - value: string 815 - $type: "#main" 816 - } 817 - | undefined 818 - value: string 819 - }`); 820 - }); 821 - 822 - test("Edge case: circular reference between multiple types", () => { 823 - const ns = lx.namespace("test", { 824 - user: lx.object({ 825 - name: lx.string({ required: true }), 826 - posts: lx.array(lx.ref("#post")), 827 - }), 828 - post: lx.object({ 829 - title: lx.string({ required: true }), 830 - author: lx.ref("#user", { required: true }), 831 - }), 832 - main: lx.object({ 833 - users: lx.array(lx.ref("#user")), 834 - }), 835 - }); 836 - 837 - attest(ns.infer).type.toString.snap(`{ 838 - $type: "test" 839 - users?: 840 - | { 841 - posts?: 842 - | { 843 - author?: 844 - | "[Circular reference detected: #user]" 845 - | undefined 846 - title: string 847 - $type: "#post" 848 - }[] 849 - | undefined 850 - name: string 851 - $type: "#user" 852 - }[] 853 - | undefined 854 - }`); 855 - }); 856 - 857 - test("Edge case: missing reference detection", () => { 858 - const ns = lx.namespace("test", { 859 - main: lx.object({ 860 - author: lx.ref("#user", { required: true }), 861 - }), 862 - }); 863 - 864 - attest(ns.infer).type.toString.snap(`{ 865 - $type: "test" 866 - author?: "[Reference not found: #user]" | undefined 867 - }`); 868 - });
-789
tests/primitives.test.ts
··· 1 - import { expect, test } from "vitest"; 2 - import { lx } from "../src/lib.ts"; 3 - 4 - test("lx.null()", () => { 5 - const result = lx.null(); 6 - expect(result).toEqual({ type: "null" }); 7 - }); 8 - 9 - test("lx.boolean()", () => { 10 - const result = lx.boolean(); 11 - expect(result).toEqual({ type: "boolean" }); 12 - }); 13 - 14 - test("lx.boolean() with default", () => { 15 - const result = lx.boolean({ default: true }); 16 - expect(result).toEqual({ type: "boolean", default: true }); 17 - }); 18 - 19 - test("lx.boolean() with const", () => { 20 - const result = lx.boolean({ const: false }); 21 - expect(result).toEqual({ type: "boolean", const: false }); 22 - }); 23 - 24 - test("lx.integer()", () => { 25 - const result = lx.integer(); 26 - expect(result).toEqual({ type: "integer" }); 27 - }); 28 - 29 - test("lx.integer() with minimum", () => { 30 - const result = lx.integer({ minimum: 0 }); 31 - expect(result).toEqual({ type: "integer", minimum: 0 }); 32 - }); 33 - 34 - test("lx.integer() with maximum", () => { 35 - const result = lx.integer({ maximum: 100 }); 36 - expect(result).toEqual({ type: "integer", maximum: 100 }); 37 - }); 38 - 39 - test("lx.integer() with minimum and maximum", () => { 40 - const result = lx.integer({ minimum: 0, maximum: 100 }); 41 - expect(result).toEqual({ type: "integer", minimum: 0, maximum: 100 }); 42 - }); 43 - 44 - test("lx.integer() with enum", () => { 45 - const result = lx.integer({ enum: [1, 2, 3, 5, 8, 13] }); 46 - expect(result).toEqual({ type: "integer", enum: [1, 2, 3, 5, 8, 13] }); 47 - }); 48 - 49 - test("lx.integer() with default", () => { 50 - const result = lx.integer({ default: 42 }); 51 - expect(result).toEqual({ type: "integer", default: 42 }); 52 - }); 53 - 54 - test("lx.integer() with const", () => { 55 - const result = lx.integer({ const: 7 }); 56 - expect(result).toEqual({ type: "integer", const: 7 }); 57 - }); 58 - 59 - test("lx.string()", () => { 60 - const result = lx.string(); 61 - expect(result).toEqual({ type: "string" }); 62 - }); 63 - 64 - test("lx.string() with maxLength", () => { 65 - const result = lx.string({ maxLength: 64 }); 66 - expect(result).toEqual({ type: "string", maxLength: 64 }); 67 - }); 68 - 69 - test("lx.string() with enum", () => { 70 - const result = lx.string({ enum: ["light", "dark", "auto"] }); 71 - expect(result).toEqual({ type: "string", enum: ["light", "dark", "auto"] }); 72 - }); 73 - 74 - test("lx.unknown()", () => { 75 - const result = lx.unknown(); 76 - expect(result).toEqual({ type: "unknown" }); 77 - }); 78 - 79 - test("lx.bytes()", () => { 80 - const result = lx.bytes(); 81 - expect(result).toEqual({ type: "bytes" }); 82 - }); 83 - 84 - test("lx.bytes() with minLength", () => { 85 - const result = lx.bytes({ minLength: 1 }); 86 - expect(result).toEqual({ type: "bytes", minLength: 1 }); 87 - }); 88 - 89 - test("lx.bytes() with maxLength", () => { 90 - const result = lx.bytes({ maxLength: 1024 }); 91 - expect(result).toEqual({ type: "bytes", maxLength: 1024 }); 92 - }); 93 - 94 - test("lx.bytes() with minLength and maxLength", () => { 95 - const result = lx.bytes({ minLength: 1, maxLength: 1024 }); 96 - expect(result).toEqual({ type: "bytes", minLength: 1, maxLength: 1024 }); 97 - }); 98 - 99 - test("lx.cidLink()", () => { 100 - const result = lx.cidLink( 101 - "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 102 - ); 103 - expect(result).toEqual({ 104 - type: "cid-link", 105 - $link: "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a", 106 - }); 107 - }); 108 - 109 - test("lx.blob()", () => { 110 - const result = lx.blob(); 111 - expect(result).toEqual({ type: "blob" }); 112 - }); 113 - 114 - test("lx.blob() with accept", () => { 115 - const result = lx.blob({ accept: ["image/png", "image/jpeg"] }); 116 - expect(result).toEqual({ 117 - type: "blob", 118 - accept: ["image/png", "image/jpeg"], 119 - }); 120 - }); 121 - 122 - test("lx.blob() with maxSize", () => { 123 - const result = lx.blob({ maxSize: 1000000 }); 124 - expect(result).toEqual({ type: "blob", maxSize: 1000000 }); 125 - }); 126 - 127 - test("lx.blob() with accept and maxSize", () => { 128 - const result = lx.blob({ 129 - accept: ["image/png", "image/jpeg"], 130 - maxSize: 5000000, 131 - }); 132 - expect(result).toEqual({ 133 - type: "blob", 134 - accept: ["image/png", "image/jpeg"], 135 - maxSize: 5000000, 136 - }); 137 - }); 138 - 139 - test("lx.array() with string items", () => { 140 - const result = lx.array(lx.string()); 141 - expect(result).toEqual({ type: "array", items: { type: "string" } }); 142 - }); 143 - 144 - test("lx.array() with integer items", () => { 145 - const result = lx.array(lx.integer()); 146 - expect(result).toEqual({ type: "array", items: { type: "integer" } }); 147 - }); 148 - 149 - test("lx.array() with minLength", () => { 150 - const result = lx.array(lx.string(), { minLength: 1 }); 151 - expect(result).toEqual({ 152 - type: "array", 153 - items: { type: "string" }, 154 - minLength: 1, 155 - }); 156 - }); 157 - 158 - test("lx.array() with maxLength", () => { 159 - const result = lx.array(lx.string(), { maxLength: 10 }); 160 - expect(result).toEqual({ 161 - type: "array", 162 - items: { type: "string" }, 163 - maxLength: 10, 164 - }); 165 - }); 166 - 167 - test("lx.array() with minLength and maxLength", () => { 168 - const result = lx.array(lx.string(), { minLength: 1, maxLength: 10 }); 169 - expect(result).toEqual({ 170 - type: "array", 171 - items: { type: "string" }, 172 - minLength: 1, 173 - maxLength: 10, 174 - }); 175 - }); 176 - 177 - test("lx.array() with required", () => { 178 - const result = lx.array(lx.string(), { required: true }); 179 - expect(result).toEqual({ 180 - type: "array", 181 - items: { type: "string" }, 182 - required: true, 183 - }); 184 - }); 185 - 186 - test("lx.token() with interaction event", () => { 187 - const result = lx.token( 188 - "Request that less content like the given feed item be shown in the feed", 189 - ); 190 - expect(result).toEqual({ 191 - type: "token", 192 - description: 193 - "Request that less content like the given feed item be shown in the feed", 194 - }); 195 - }); 196 - 197 - test("lx.token() with content mode", () => { 198 - const result = lx.token( 199 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds", 200 - ); 201 - expect(result).toEqual({ 202 - type: "token", 203 - description: 204 - "Declares the feed generator returns posts containing app.bsky.embed.video embeds", 205 - }); 206 - }); 207 - 208 - test("lx.ref() with local definition", () => { 209 - const result = lx.ref("#profileAssociated"); 210 - expect(result).toEqual({ 211 - type: "ref", 212 - ref: "#profileAssociated", 213 - }); 214 - }); 215 - 216 - test("lx.ref() with external schema", () => { 217 - const result = lx.ref("com.atproto.label.defs#label"); 218 - expect(result).toEqual({ 219 - type: "ref", 220 - ref: "com.atproto.label.defs#label", 221 - }); 222 - }); 223 - 224 - test("lx.ref() with required option", () => { 225 - const result = lx.ref("#profileView", { required: true }); 226 - expect(result).toEqual({ 227 - type: "ref", 228 - ref: "#profileView", 229 - required: true, 230 - }); 231 - }); 232 - 233 - test("lx.ref() with nullable option", () => { 234 - const result = lx.ref("#profileView", { nullable: true }); 235 - expect(result).toEqual({ 236 - type: "ref", 237 - ref: "#profileView", 238 - nullable: true, 239 - }); 240 - }); 241 - 242 - test("lx.ref() with both required and nullable", () => { 243 - const result = lx.ref("app.bsky.actor.defs#profileView", { 244 - required: true, 245 - nullable: true, 246 - }); 247 - expect(result).toEqual({ 248 - type: "ref", 249 - ref: "app.bsky.actor.defs#profileView", 250 - required: true, 251 - nullable: true, 252 - }); 253 - }); 254 - 255 - test("lx.union() with local refs", () => { 256 - const result = lx.union(["#reasonRepost", "#reasonPin"]); 257 - expect(result).toEqual({ 258 - type: "union", 259 - refs: ["#reasonRepost", "#reasonPin"], 260 - }); 261 - }); 262 - 263 - test("lx.union() with external refs", () => { 264 - const result = lx.union([ 265 - "app.bsky.embed.images#view", 266 - "app.bsky.embed.video#view", 267 - "app.bsky.embed.external#view", 268 - "app.bsky.embed.record#view", 269 - "app.bsky.embed.recordWithMedia#view", 270 - ]); 271 - expect(result).toEqual({ 272 - type: "union", 273 - refs: [ 274 - "app.bsky.embed.images#view", 275 - "app.bsky.embed.video#view", 276 - "app.bsky.embed.external#view", 277 - "app.bsky.embed.record#view", 278 - "app.bsky.embed.recordWithMedia#view", 279 - ], 280 - }); 281 - }); 282 - 283 - test("lx.union() with closed option", () => { 284 - const result = lx.union(["#postView", "#notFoundPost", "#blockedPost"], { 285 - closed: true, 286 - }); 287 - expect(result).toEqual({ 288 - type: "union", 289 - refs: ["#postView", "#notFoundPost", "#blockedPost"], 290 - closed: true, 291 - }); 292 - }); 293 - 294 - test("lx.union() with closed: false (open union)", () => { 295 - const result = lx.union(["#threadViewPost", "#notFoundPost"], { 296 - closed: false, 297 - }); 298 - expect(result).toEqual({ 299 - type: "union", 300 - refs: ["#threadViewPost", "#notFoundPost"], 301 - closed: false, 302 - }); 303 - }); 304 - 305 - test("lx.params() with basic properties", () => { 306 - const result = lx.params({ 307 - q: lx.string(), 308 - limit: lx.integer(), 309 - }); 310 - expect(result).toEqual({ 311 - type: "params", 312 - properties: { 313 - q: { type: "string" }, 314 - limit: { type: "integer" }, 315 - }, 316 - }); 317 - }); 318 - 319 - test("lx.params() with required properties", () => { 320 - const result = lx.params({ 321 - q: lx.string({ required: true }), 322 - limit: lx.integer(), 323 - }); 324 - expect(result).toEqual({ 325 - type: "params", 326 - properties: { 327 - q: { type: "string", required: true }, 328 - limit: { type: "integer" }, 329 - }, 330 - required: ["q"], 331 - }); 332 - }); 333 - 334 - test("lx.params() with property options", () => { 335 - const result = lx.params({ 336 - q: lx.string(), 337 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 338 - cursor: lx.string(), 339 - }); 340 - expect(result).toEqual({ 341 - type: "params", 342 - properties: { 343 - q: { type: "string" }, 344 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 345 - cursor: { type: "string" }, 346 - }, 347 - }); 348 - }); 349 - 350 - test("lx.params() with array properties", () => { 351 - const result = lx.params({ 352 - tags: lx.array(lx.string()), 353 - ids: lx.array(lx.integer()), 354 - }); 355 - expect(result).toEqual({ 356 - type: "params", 357 - properties: { 358 - tags: { type: "array", items: { type: "string" } }, 359 - ids: { type: "array", items: { type: "integer" } }, 360 - }, 361 - }); 362 - }); 363 - 364 - test("lx.params() real-world example from searchActors", () => { 365 - const result = lx.params({ 366 - q: lx.string({ required: true }), 367 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 368 - cursor: lx.string(), 369 - }); 370 - expect(result).toEqual({ 371 - type: "params", 372 - properties: { 373 - q: { type: "string", required: true }, 374 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 375 - cursor: { type: "string" }, 376 - }, 377 - required: ["q"], 378 - }); 379 - }); 380 - 381 - test("lx.query() basic", () => { 382 - const result = lx.query(); 383 - expect(result).toEqual({ type: "query" }); 384 - }); 385 - 386 - test("lx.query() with description", () => { 387 - const result = lx.query({ description: "Search for actors" }); 388 - expect(result).toEqual({ type: "query", description: "Search for actors" }); 389 - }); 390 - 391 - test("lx.query() with parameters", () => { 392 - const result = lx.query({ 393 - parameters: lx.params({ 394 - q: lx.string({ required: true }), 395 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 396 - }), 397 - }); 398 - expect(result).toEqual({ 399 - type: "query", 400 - parameters: { 401 - type: "params", 402 - properties: { 403 - q: { type: "string", required: true }, 404 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 405 - }, 406 - required: ["q"], 407 - }, 408 - }); 409 - }); 410 - 411 - test("lx.query() with output", () => { 412 - const result = lx.query({ 413 - output: { 414 - encoding: "application/json", 415 - schema: lx.object({ 416 - posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { 417 - required: true, 418 - }), 419 - cursor: lx.string(), 420 - }), 421 - }, 422 - }); 423 - expect(result).toEqual({ 424 - type: "query", 425 - output: { 426 - encoding: "application/json", 427 - schema: { 428 - type: "object", 429 - properties: { 430 - posts: { 431 - type: "array", 432 - items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 433 - required: true, 434 - }, 435 - cursor: { type: "string" }, 436 - }, 437 - required: ["posts"], 438 - }, 439 - }, 440 - }); 441 - }); 442 - 443 - test("lx.query() with errors", () => { 444 - const result = lx.query({ 445 - errors: [{ name: "BadQueryString" }], 446 - }); 447 - expect(result).toEqual({ 448 - type: "query", 449 - errors: [{ name: "BadQueryString" }], 450 - }); 451 - }); 452 - 453 - test("lx.query() real-world example: searchPosts", () => { 454 - const result = lx.query({ 455 - description: "Find posts matching search criteria", 456 - parameters: lx.params({ 457 - q: lx.string({ required: true }), 458 - sort: lx.string({ enum: ["top", "latest"], default: "latest" }), 459 - limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }), 460 - cursor: lx.string(), 461 - }), 462 - output: { 463 - encoding: "application/json", 464 - schema: lx.object({ 465 - cursor: lx.string(), 466 - hitsTotal: lx.integer(), 467 - posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { 468 - required: true, 469 - }), 470 - }), 471 - }, 472 - errors: [{ name: "BadQueryString" }], 473 - }); 474 - expect(result).toEqual({ 475 - type: "query", 476 - description: "Find posts matching search criteria", 477 - parameters: { 478 - type: "params", 479 - properties: { 480 - q: { type: "string", required: true }, 481 - sort: { type: "string", enum: ["top", "latest"], default: "latest" }, 482 - limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }, 483 - cursor: { type: "string" }, 484 - }, 485 - required: ["q"], 486 - }, 487 - output: { 488 - encoding: "application/json", 489 - schema: { 490 - type: "object", 491 - properties: { 492 - cursor: { type: "string" }, 493 - hitsTotal: { type: "integer" }, 494 - posts: { 495 - type: "array", 496 - items: { type: "ref", ref: "app.bsky.feed.defs#postView" }, 497 - required: true, 498 - }, 499 - }, 500 - required: ["posts"], 501 - }, 502 - }, 503 - errors: [{ name: "BadQueryString" }], 504 - }); 505 - }); 506 - 507 - test("lx.procedure() basic", () => { 508 - const result = lx.procedure(); 509 - expect(result).toEqual({ type: "procedure" }); 510 - }); 511 - 512 - test("lx.procedure() with description", () => { 513 - const result = lx.procedure({ description: "Create a new post" }); 514 - expect(result).toEqual({ 515 - type: "procedure", 516 - description: "Create a new post", 517 - }); 518 - }); 519 - 520 - test("lx.procedure() with parameters", () => { 521 - const result = lx.procedure({ 522 - parameters: lx.params({ 523 - validate: lx.boolean({ default: true }), 524 - }), 525 - }); 526 - expect(result).toEqual({ 527 - type: "procedure", 528 - parameters: { 529 - type: "params", 530 - properties: { 531 - validate: { type: "boolean", default: true }, 532 - }, 533 - }, 534 - }); 535 - }); 536 - 537 - test("lx.procedure() with input", () => { 538 - const result = lx.procedure({ 539 - input: { 540 - encoding: "application/json", 541 - schema: lx.object({ 542 - text: lx.string({ required: true, maxGraphemes: 300 }), 543 - createdAt: lx.string({ format: "datetime" }), 544 - }), 545 - }, 546 - }); 547 - expect(result).toEqual({ 548 - type: "procedure", 549 - input: { 550 - encoding: "application/json", 551 - schema: { 552 - type: "object", 553 - properties: { 554 - text: { type: "string", required: true, maxGraphemes: 300 }, 555 - createdAt: { type: "string", format: "datetime" }, 556 - }, 557 - required: ["text"], 558 - }, 559 - }, 560 - }); 561 - }); 562 - 563 - test("lx.procedure() with output", () => { 564 - const result = lx.procedure({ 565 - output: { 566 - encoding: "application/json", 567 - schema: lx.object({ 568 - uri: lx.string({ required: true }), 569 - cid: lx.string({ required: true }), 570 - }), 571 - }, 572 - }); 573 - expect(result).toEqual({ 574 - type: "procedure", 575 - output: { 576 - encoding: "application/json", 577 - schema: { 578 - type: "object", 579 - properties: { 580 - uri: { type: "string", required: true }, 581 - cid: { type: "string", required: true }, 582 - }, 583 - required: ["uri", "cid"], 584 - }, 585 - }, 586 - }); 587 - }); 588 - 589 - test("lx.procedure() with errors", () => { 590 - const result = lx.procedure({ 591 - errors: [ 592 - { name: "InvalidRequest" }, 593 - { name: "RateLimitExceeded", description: "Too many requests" }, 594 - ], 595 - }); 596 - expect(result).toEqual({ 597 - type: "procedure", 598 - errors: [ 599 - { name: "InvalidRequest" }, 600 - { name: "RateLimitExceeded", description: "Too many requests" }, 601 - ], 602 - }); 603 - }); 604 - 605 - test("lx.procedure() real-world example: createPost", () => { 606 - const result = lx.procedure({ 607 - description: "Create a post", 608 - input: { 609 - encoding: "application/json", 610 - schema: lx.object({ 611 - repo: lx.string({ required: true }), 612 - collection: lx.string({ required: true }), 613 - record: lx.unknown({ required: true }), 614 - validate: lx.boolean({ default: true }), 615 - }), 616 - }, 617 - output: { 618 - encoding: "application/json", 619 - schema: lx.object({ 620 - uri: lx.string({ required: true }), 621 - cid: lx.string({ required: true }), 622 - }), 623 - }, 624 - errors: [{ name: "InvalidSwap" }, { name: "InvalidRecord" }], 625 - }); 626 - expect(result).toEqual({ 627 - type: "procedure", 628 - description: "Create a post", 629 - input: { 630 - encoding: "application/json", 631 - schema: { 632 - type: "object", 633 - properties: { 634 - repo: { type: "string", required: true }, 635 - collection: { type: "string", required: true }, 636 - record: { type: "unknown", required: true }, 637 - validate: { type: "boolean", default: true }, 638 - }, 639 - required: ["repo", "collection", "record"], 640 - }, 641 - }, 642 - output: { 643 - encoding: "application/json", 644 - schema: { 645 - type: "object", 646 - properties: { 647 - uri: { type: "string", required: true }, 648 - cid: { type: "string", required: true }, 649 - }, 650 - required: ["uri", "cid"], 651 - }, 652 - }, 653 - errors: [{ name: "InvalidSwap" }, { name: "InvalidRecord" }], 654 - }); 655 - }); 656 - 657 - test("lx.subscription() basic", () => { 658 - const result = lx.subscription(); 659 - expect(result).toEqual({ type: "subscription" }); 660 - }); 661 - 662 - test("lx.subscription() with description", () => { 663 - const result = lx.subscription({ 664 - description: "Repository event stream", 665 - }); 666 - expect(result).toEqual({ 667 - type: "subscription", 668 - description: "Repository event stream", 669 - }); 670 - }); 671 - 672 - test("lx.subscription() with parameters", () => { 673 - const result = lx.subscription({ 674 - parameters: lx.params({ 675 - cursor: lx.integer(), 676 - }), 677 - }); 678 - expect(result).toEqual({ 679 - type: "subscription", 680 - parameters: { 681 - type: "params", 682 - properties: { 683 - cursor: { type: "integer" }, 684 - }, 685 - }, 686 - }); 687 - }); 688 - 689 - test("lx.subscription() with message", () => { 690 - const result = lx.subscription({ 691 - message: { 692 - schema: lx.union(["#commit", "#identity", "#account"]), 693 - }, 694 - }); 695 - expect(result).toEqual({ 696 - type: "subscription", 697 - message: { 698 - schema: { 699 - type: "union", 700 - refs: ["#commit", "#identity", "#account"], 701 - }, 702 - }, 703 - }); 704 - }); 705 - 706 - test("lx.subscription() with message description", () => { 707 - const result = lx.subscription({ 708 - message: { 709 - description: "Event message types", 710 - schema: lx.union(["#commit", "#handle", "#migrate"]), 711 - }, 712 - }); 713 - expect(result).toEqual({ 714 - type: "subscription", 715 - message: { 716 - description: "Event message types", 717 - schema: { 718 - type: "union", 719 - refs: ["#commit", "#handle", "#migrate"], 720 - }, 721 - }, 722 - }); 723 - }); 724 - 725 - test("lx.subscription() with errors", () => { 726 - const result = lx.subscription({ 727 - errors: [ 728 - { name: "FutureCursor" }, 729 - { name: "ConsumerTooSlow", description: "Consumer is too slow" }, 730 - ], 731 - }); 732 - expect(result).toEqual({ 733 - type: "subscription", 734 - errors: [ 735 - { name: "FutureCursor" }, 736 - { name: "ConsumerTooSlow", description: "Consumer is too slow" }, 737 - ], 738 - }); 739 - }); 740 - 741 - test("lx.subscription() real-world example: subscribeRepos", () => { 742 - const result = lx.subscription({ 743 - description: "Repository event stream, aka Firehose endpoint", 744 - parameters: lx.params({ 745 - cursor: lx.integer(), 746 - }), 747 - message: { 748 - description: "Represents an update of repository state", 749 - schema: lx.union([ 750 - "#commit", 751 - "#identity", 752 - "#account", 753 - "#handle", 754 - "#migrate", 755 - "#tombstone", 756 - "#info", 757 - ]), 758 - }, 759 - errors: [{ name: "FutureCursor" }, { name: "ConsumerTooSlow" }], 760 - }); 761 - expect(result).toEqual({ 762 - type: "subscription", 763 - description: "Repository event stream, aka Firehose endpoint", 764 - parameters: { 765 - type: "params", 766 - properties: { 767 - cursor: { 768 - type: "integer", 769 - }, 770 - }, 771 - }, 772 - message: { 773 - description: "Represents an update of repository state", 774 - schema: { 775 - type: "union", 776 - refs: [ 777 - "#commit", 778 - "#identity", 779 - "#account", 780 - "#handle", 781 - "#migrate", 782 - "#tombstone", 783 - "#info", 784 - ], 785 - }, 786 - }, 787 - errors: [{ name: "FutureCursor" }, { name: "ConsumerTooSlow" }], 788 - }); 789 - });