tangled
alpha
login
or
join now
tylur.dev
/
prototypey
prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey
1
fork
atom
overview
issues
pulls
pipelines
cleanup old files
Tyler
3 months ago
e9acbecd
ebdfa22f
+112
-4355
15 changed files
expand all
collapse all
unified
split
.gitignore
aislop
plan-emit.md
src
cli
commands
gen-inferred.ts
index.ts
templates
inferred.ts
index.ts
infer.ts
lib.ts
type-utils.ts
tests
base-case.test.ts
bsky-actor.test.ts
bsky-feed.test.ts
infer.bench.ts
infer.test.ts
primitives.test.ts
+4
-3
.gitignore
···
1
-
lib
2
-
node_modules
3
-
/.attest
0
···
1
+
lib/
2
+
node_modules/
3
+
.attest/
4
+
generated/
+108
-94
aislop/plan-emit.md
···
52
53
```json
54
{
55
-
"bin": {
56
-
"prototypey": "./lib/cli/index.js"
57
-
},
58
-
"scripts": {
59
-
"codegen": "prototypey gen-inferred ./generated/inferred ./lexicons/**/*.json"
60
-
}
61
}
62
```
63
···
73
74
```typescript
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' }
78
79
-
export type Post = Infer<typeof schema>
80
81
// Minimal runtime helpers
82
-
export const PostSchema = schema
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
-
}
0
0
0
0
87
```
88
89
Benefits:
···
97
98
```json
99
{
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
-
}
111
}
112
```
113
···
117
118
```json
119
{
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
-
}
126
}
127
```
128
···
132
133
```json
134
{
135
-
"lexicons": "./lexicons",
136
-
"output": {
137
-
"inferred": "./generated/inferred",
138
-
"types": "./generated/types"
139
-
},
140
-
"include": ["**/*.json"],
141
-
"exclude": ["**/node_modules/**"]
142
}
143
```
144
···
170
171
1. ✅ **Phase 1**: Basic CLI structure + Track B (inferred generation) - COMPLETE
172
2. ✅ **Phase 2**: File organization + output directory structure - COMPLETE
173
-
3. ✅ **Phase 3**: Convert to pnpm workspaces monorepo - COMPLETE
174
4. **Phase 4**: Track A (standard generation, delegate to lex-cli)
175
5. **Phase 5**: Configuration file support
176
6. **Phase 6**: Documentation + examples
···
180
### ✅ Completed (2025-10-16)
181
182
**Tech Stack Choices:**
0
183
- Used `sade` instead of `commander` (modern, minimal CLI framework from awesome-e18e)
184
- Used `tinyglobby` instead of `glob` (faster, modern alternative)
185
- Built with `tsdown` for CLI bundling
186
187
**Structure Created:**
0
188
```
189
prototypey/
190
├── src/cli/
···
200
```
201
202
**Generated Code Pattern:**
0
203
```typescript
204
// generated/inferred/app/bsky/actor/profile.ts
205
import type { Infer } from "prototypey";
···
211
```
212
213
**CLI Usage:**
0
214
```bash
215
# Build CLI
216
pnpm build:cli
···
223
```
224
225
**Key Features:**
0
226
- Converts NSID to file paths: `app.bsky.feed.post` → `app/bsky/feed/post.ts`
227
- Generates minimal runtime code with type inference
228
- Auto-creates directory structure
···
230
- Type guard functions for runtime checks
231
232
**Testing:**
0
233
- Successfully generated types from sample lexicons
234
- Runtime validation works (tested with node)
235
- Schema imports work correctly with JSON modules
···
281
### Package Configurations
282
283
**Root `pnpm-workspace.yaml`:**
0
284
```yaml
285
packages:
286
-
- 'packages/*'
287
```
288
289
**Root `package.json`:**
0
290
```json
291
{
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
-
}
300
}
301
```
302
303
**`packages/prototypey/package.json`:**
0
304
```json
305
{
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
-
}
318
}
319
```
320
321
**`packages/cli/package.json`:**
0
322
```json
323
{
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
-
}
337
}
338
```
339
···
442
/**
443
* GENERATED CODE - DO NOT MODIFY
444
*/
445
-
import { ValidationResult, BlobRef } from '@atproto/lexicon'
446
-
import { lexicons } from '../../../../lexicons'
447
-
import { isObj, hasProp } from '../../../../util'
448
-
import { CID } from 'multiformats/cid'
449
450
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
455
}
456
457
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
-
)
464
}
465
466
export function validateMain(v: unknown): ValidationResult {
467
-
return lexicons.validate('app.bsky.richtext.facet#main', v)
468
}
469
```
470
···
474
475
```json
476
{
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
-
}
484
}
485
```
486
···
52
53
```json
54
{
55
+
"bin": {
56
+
"prototypey": "./lib/cli/index.js"
57
+
},
58
+
"scripts": {
59
+
"codegen": "prototypey gen-inferred ./generated/inferred ./lexicons/**/*.json"
60
+
}
61
}
62
```
63
···
73
74
```typescript
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" };
78
79
+
export type Post = Infer<typeof schema>;
80
81
// Minimal runtime helpers
82
+
export const PostSchema = schema;
83
export const isPost = (v: unknown): v is Post => {
84
+
return (
85
+
typeof v === "object" &&
86
+
v !== null &&
87
+
"$type" in v &&
88
+
v.$type === "app.bsky.feed.post"
89
+
);
90
+
};
91
```
92
93
Benefits:
···
101
102
```json
103
{
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
+
}
115
}
116
```
117
···
121
122
```json
123
{
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
+
}
130
}
131
```
132
···
136
137
```json
138
{
139
+
"lexicons": "./lexicons",
140
+
"output": {
141
+
"inferred": "./generated/inferred",
142
+
"types": "./generated/types"
143
+
},
144
+
"include": ["**/*.json"],
145
+
"exclude": ["**/node_modules/**"]
146
}
147
```
148
···
174
175
1. ✅ **Phase 1**: Basic CLI structure + Track B (inferred generation) - COMPLETE
176
2. ✅ **Phase 2**: File organization + output directory structure - COMPLETE
177
+
3. ✅ **Phase 3**: Convert to pnpm workspaces monorepo - COMPLETE - this was marked complete but we still have src and packages
178
4. **Phase 4**: Track A (standard generation, delegate to lex-cli)
179
5. **Phase 5**: Configuration file support
180
6. **Phase 6**: Documentation + examples
···
184
### ✅ Completed (2025-10-16)
185
186
**Tech Stack Choices:**
187
+
188
- Used `sade` instead of `commander` (modern, minimal CLI framework from awesome-e18e)
189
- Used `tinyglobby` instead of `glob` (faster, modern alternative)
190
- Built with `tsdown` for CLI bundling
191
192
**Structure Created:**
193
+
194
```
195
prototypey/
196
├── src/cli/
···
206
```
207
208
**Generated Code Pattern:**
209
+
210
```typescript
211
// generated/inferred/app/bsky/actor/profile.ts
212
import type { Infer } from "prototypey";
···
218
```
219
220
**CLI Usage:**
221
+
222
```bash
223
# Build CLI
224
pnpm build:cli
···
231
```
232
233
**Key Features:**
234
+
235
- Converts NSID to file paths: `app.bsky.feed.post` → `app/bsky/feed/post.ts`
236
- Generates minimal runtime code with type inference
237
- Auto-creates directory structure
···
239
- Type guard functions for runtime checks
240
241
**Testing:**
242
+
243
- Successfully generated types from sample lexicons
244
- Runtime validation works (tested with node)
245
- Schema imports work correctly with JSON modules
···
291
### Package Configurations
292
293
**Root `pnpm-workspace.yaml`:**
294
+
295
```yaml
296
packages:
297
+
- "packages/*"
298
```
299
300
**Root `package.json`:**
301
+
302
```json
303
{
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
+
}
312
}
313
```
314
315
**`packages/prototypey/package.json`:**
316
+
317
```json
318
{
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
+
}
331
}
332
```
333
334
**`packages/cli/package.json`:**
335
+
336
```json
337
{
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
+
}
351
}
352
```
353
···
456
/**
457
* GENERATED CODE - DO NOT MODIFY
458
*/
459
+
import { ValidationResult, BlobRef } from "@atproto/lexicon";
460
+
import { lexicons } from "../../../../lexicons";
461
+
import { isObj, hasProp } from "../../../../util";
462
+
import { CID } from "multiformats/cid";
463
464
export interface Main {
465
+
$type?: "app.bsky.richtext.facet";
466
+
index: ByteSlice;
467
+
features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[];
468
+
[k: string]: unknown;
469
}
470
471
export function isMain(v: unknown): v is Main {
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
+
);
478
}
479
480
export function validateMain(v: unknown): ValidationResult {
481
+
return lexicons.validate("app.bsky.richtext.facet#main", v);
482
}
483
```
484
···
488
489
```json
490
{
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
+
}
498
}
499
```
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
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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);
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-2
src/index.ts
···
1
-
export * from "./lib.ts";
2
-
export * from "./infer.ts";
···
0
0
-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
-
>;
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
};
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
} & {};
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
});
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
});
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
});
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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"]);
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
});
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
});
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0