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