@atproto/lex-schema 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @atproto/lex-schema
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5037](https://github.com/bluesky-social/atproto/pull/5037) [`7b8ca6e`](https://github.com/bluesky-social/atproto/commit/7b8ca6e0aace79cca38e880429317fde8268ba50) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add `atUri()` helper for safely building `AtUriString` values from raw components or record schemas. Also fixes validation of `'any'` record-key strings.
8
+
3
9
  ## 0.1.2
4
10
 
5
11
  ### Patch Changes
@@ -1,3 +1,4 @@
1
+ import { NsidString, TidString } from '@atproto/syntax';
1
2
  /**
2
3
  * The valid record key constraint types in a lexicon definition.
3
4
  *
@@ -45,4 +46,13 @@ export declare function isLexiconRecordKey<T>(key: T): key is T & LexiconRecordK
45
46
  * ```
46
47
  */
47
48
  export declare function asLexiconRecordKey(key: unknown): LexiconRecordKey;
49
+ /**
50
+ * Maps a lexicon record key definition to its corresponding string subtype.
51
+ *
52
+ * - `'any'` maps to `string`
53
+ * - `'nsid'` maps to `NsidString`
54
+ * - `'tid'` maps to `TidString`
55
+ * - `'literal:...'` maps to the literal string value
56
+ */
57
+ export type RecordKeyValue<Key extends LexiconRecordKey = LexiconRecordKey> = Key extends 'any' ? string : Key extends 'tid' ? TidString : Key extends 'nsid' ? NsidString : Key extends `literal:${infer L extends string}` ? L : never;
48
58
  //# sourceMappingURL=record-key.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"record-key.d.ts","sourceRoot":"","sources":["../../src/core/record-key.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,WAAW,MAAM,EAAE,CAAA;AAE3E;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,gBAAgB,CAUzE;AAED;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAGjE"}
1
+ {"version":3,"file":"record-key.d.ts","sourceRoot":"","sources":["../../src/core/record-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAEzE;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,WAAW,MAAM,EAAE,CAAA;AAE3E;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,gBAAgB,CAUzE;AAED;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CAGjE;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,CAAC,GAAG,SAAS,gBAAgB,GAAG,gBAAgB,IACxE,GAAG,SAAS,KAAK,GACb,MAAM,GACN,GAAG,SAAS,KAAK,GACf,SAAS,GACT,GAAG,SAAS,MAAM,GAChB,UAAU,GACV,GAAG,SAAS,WAAW,MAAM,CAAC,SAAS,MAAM,EAAE,GAC7C,CAAC,GACD,KAAK,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"record-key.js","sourceRoot":"","sources":["../../src/core/record-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAkBlD;;;;;;;;;;;;;;GAcG;AACH,wBAAwB;AACxB,MAAM,UAAU,kBAAkB,CAAI,GAAM;IAC1C,OAAO,CACL,GAAG,KAAK,KAAK;QACb,GAAG,KAAK,MAAM;QACd,GAAG,KAAK,KAAK;QACb,CAAC,OAAO,GAAG,KAAK,QAAQ;YACtB,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;YAC1B,GAAG,CAAC,MAAM,GAAG,CAAC;YACd,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAClC,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAwB;AACxB,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,kBAAkB,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAA;IACvC,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AACvD,CAAC","sourcesContent":["import { isValidRecordKey } from '@atproto/syntax'\n\n/**\n * The valid record key constraint types in a lexicon definition.\n *\n * - `'any'` - Accepts any valid record key\n * - `'nsid'` - Record key must be a valid NSID\n * - `'tid'` - Record key must be a valid TID\n * - `'literal:...'` - Record key must be the exact specified value\n *\n * @example\n * ```typescript\n * const constraint: LexiconRecordKey = 'tid'\n * const literalConstraint: LexiconRecordKey = 'literal:self'\n * ```\n */\nexport type LexiconRecordKey = 'any' | 'nsid' | 'tid' | `literal:${string}`\n\n/**\n * Type guard that checks if a value is a valid lexicon record key constraint.\n *\n * @typeParam T - The input type\n * @param key - The value to check\n * @returns `true` if the value is a valid record key constraint\n *\n * @example\n * ```typescript\n * if (isLexiconRecordKey(value)) {\n * // value is typed as LexiconRecordKey\n * console.log('Valid constraint:', value)\n * }\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function isLexiconRecordKey<T>(key: T): key is T & LexiconRecordKey {\n return (\n key === 'any' ||\n key === 'nsid' ||\n key === 'tid' ||\n (typeof key === 'string' &&\n key.startsWith('literal:') &&\n key.length > 8 &&\n isValidRecordKey(key.slice(8)))\n )\n}\n\n/**\n * Validates and returns a value as a lexicon record key constraint, throwing if invalid.\n *\n * @param key - The value to validate\n * @returns The value typed as {@link LexiconRecordKey}\n * @throws {Error} If the value is not a valid record key constraint\n *\n * @example\n * ```typescript\n * const constraint = asLexiconRecordKey('tid')\n * // constraint is typed as LexiconRecordKey\n *\n * asLexiconRecordKey('invalid') // throws Error\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function asLexiconRecordKey(key: unknown): LexiconRecordKey {\n if (isLexiconRecordKey(key)) return key\n throw new Error(`Invalid record key: ${String(key)}`)\n}\n"]}
1
+ {"version":3,"file":"record-key.js","sourceRoot":"","sources":["../../src/core/record-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAkBzE;;;;;;;;;;;;;;GAcG;AACH,wBAAwB;AACxB,MAAM,UAAU,kBAAkB,CAAI,GAAM;IAC1C,OAAO,CACL,GAAG,KAAK,KAAK;QACb,GAAG,KAAK,MAAM;QACd,GAAG,KAAK,KAAK;QACb,CAAC,OAAO,GAAG,KAAK,QAAQ;YACtB,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC;YAC1B,GAAG,CAAC,MAAM,GAAG,CAAC;YACd,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAClC,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAwB;AACxB,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,IAAI,kBAAkB,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAA;IACvC,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AACvD,CAAC","sourcesContent":["import { NsidString, TidString, isValidRecordKey } from '@atproto/syntax'\n\n/**\n * The valid record key constraint types in a lexicon definition.\n *\n * - `'any'` - Accepts any valid record key\n * - `'nsid'` - Record key must be a valid NSID\n * - `'tid'` - Record key must be a valid TID\n * - `'literal:...'` - Record key must be the exact specified value\n *\n * @example\n * ```typescript\n * const constraint: LexiconRecordKey = 'tid'\n * const literalConstraint: LexiconRecordKey = 'literal:self'\n * ```\n */\nexport type LexiconRecordKey = 'any' | 'nsid' | 'tid' | `literal:${string}`\n\n/**\n * Type guard that checks if a value is a valid lexicon record key constraint.\n *\n * @typeParam T - The input type\n * @param key - The value to check\n * @returns `true` if the value is a valid record key constraint\n *\n * @example\n * ```typescript\n * if (isLexiconRecordKey(value)) {\n * // value is typed as LexiconRecordKey\n * console.log('Valid constraint:', value)\n * }\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function isLexiconRecordKey<T>(key: T): key is T & LexiconRecordKey {\n return (\n key === 'any' ||\n key === 'nsid' ||\n key === 'tid' ||\n (typeof key === 'string' &&\n key.startsWith('literal:') &&\n key.length > 8 &&\n isValidRecordKey(key.slice(8)))\n )\n}\n\n/**\n * Validates and returns a value as a lexicon record key constraint, throwing if invalid.\n *\n * @param key - The value to validate\n * @returns The value typed as {@link LexiconRecordKey}\n * @throws {Error} If the value is not a valid record key constraint\n *\n * @example\n * ```typescript\n * const constraint = asLexiconRecordKey('tid')\n * // constraint is typed as LexiconRecordKey\n *\n * asLexiconRecordKey('invalid') // throws Error\n * ```\n */\n/*@__NO_SIDE_EFFECTS__*/\nexport function asLexiconRecordKey(key: unknown): LexiconRecordKey {\n if (isLexiconRecordKey(key)) return key\n throw new Error(`Invalid record key: ${String(key)}`)\n}\n\n/**\n * Maps a lexicon record key definition to its corresponding string subtype.\n *\n * - `'any'` maps to `string`\n * - `'nsid'` maps to `NsidString`\n * - `'tid'` maps to `TidString`\n * - `'literal:...'` maps to the literal string value\n */\nexport type RecordKeyValue<Key extends LexiconRecordKey = LexiconRecordKey> =\n Key extends 'any'\n ? string\n : Key extends 'tid'\n ? TidString\n : Key extends 'nsid'\n ? NsidString\n : Key extends `literal:${infer L extends string}`\n ? L\n : never\n"]}
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { InferOutput, Restricted } from './core.js';
2
- import { InferPayload, InferPayloadBody, InferPayloadEncoding, Procedure, Query, Subscription } from './schema.js';
1
+ import { AtIdentifierString, InferOutput, NsidString, RecordKeyValue, Restricted } from './core.js';
2
+ import { InferPayload, InferPayloadBody, InferPayloadEncoding, InferRecordKey, Procedure, Query, RecordSchema, Subscription } from './schema.js';
3
3
  export type Main<T> = T | {
4
4
  main: T;
5
5
  };
@@ -31,4 +31,37 @@ export declare const lexErrorDataSchema: import("./schema.js").ObjectSchema<{
31
31
  readonly error: import("./schema.js").RegexpSchema<string>;
32
32
  readonly message: import("./schema.js").OptionalSchema<import("./schema.js").StringSchema<{}>>;
33
33
  }>;
34
+ /**
35
+ * Helper function to construct AT Protocol URIs with compile-time & runtime
36
+ * validation of their components. This function supports different use cases,
37
+ * including constructing URIs from raw strings or from RecordSchema instances,
38
+ * ensuring that the resulting URI adheres to the expected format.
39
+ *
40
+ * @throws {TypeError} If the arguments do not match the interface
41
+ * @throws {Error} If AT-URI components are invalid
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { atUri } from '@atproto/lex'
46
+ * import { app } from '#/lexicons/index.js'
47
+ *
48
+ * // Constructing a URI from raw components
49
+ * const uri1 = atUri('did:example:123', 'app.bsky.feed.post', 'my-post')
50
+ *
51
+ * // Constructing a URI from a RecordSchema instance
52
+ * const uri2 = atUri('did:example:123', app.bsky.feed.post, 'my-post')
53
+ *
54
+ * // Literal rkey can be omitted
55
+ * const uri3 = atUri('did:example:123', app.bsky.actor.profile) // rkey 'self' is implied
56
+ *
57
+ * // Invalid URIs will throw errors
58
+ * atUri('invalid authority', 'app.bsky.feed.post', 'my-post') // throws
59
+ * atUri('did:example:123', 'invalid collection', 'my-post') // throws
60
+ * atUri('did:example:123', 'app.bsky.feed.post', '..') // throws
61
+ * ```
62
+ */
63
+ export declare function atUri<const TAuthority extends AtIdentifierString>(authority: TAuthority): `at://${TAuthority}`;
64
+ export declare function atUri<const TAuthority extends AtIdentifierString, const TCollection extends NsidString, const TRecordKey extends RecordKeyValue>(authority: TAuthority, nsid: TCollection, rkey: TRecordKey extends '..' | '.' ? never : TRecordKey): `at://${TAuthority}/${TCollection}/${TRecordKey}`;
65
+ export declare function atUri<const TAuthority extends AtIdentifierString, const TRecord extends RecordSchema>(authority: TAuthority, record: TRecord['key'] extends `literal:${string}` ? Main<TRecord> : never): `at://${TAuthority}/${TRecord['$type']}/${InferRecordKey<TRecord>}`;
66
+ export declare function atUri<const TAuthority extends AtIdentifierString, const TRecord extends RecordSchema, const TRecordKey extends InferRecordKey<TRecord>>(authority: TAuthority, record: Main<TRecord>, rkey: TRecordKey extends '..' | '.' ? never : TRecordKey): `at://${TAuthority}/${TRecord['$type']}/${TRecordKey}`;
34
67
  //# sourceMappingURL=helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,UAAU,EACV,cAAc,EACd,UAAU,EAIX,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,SAAS,EACT,KAAK,EACL,YAAY,EACZ,YAAY,EAKb,MAAM,aAAa,CAAA;AAEpB,MAAM,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,CAAA;AAErC,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAExD;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,aAAa,CAAC,CAAA;AAElD,MAAM,MAAM,iBAAiB,CAC3B,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,IAE7E,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAClD,WAAW,CAAC,OAAO,CAAC,GACpB,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAC3C,WAAW,CAAC,OAAO,CAAC,GACpB,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,GAClD,WAAW,CAAC,OAAO,CAAC,GACpB,KAAK,CAAA;AAEf,MAAM,MAAM,gBAAgB,CAC1B,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,EAC7E,CAAC,GAAG,UAAU,IAEd,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GACjD,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,GACvB,SAAS,CAAA;AAEf,MAAM,MAAM,oBAAoB,CAC9B,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,EAC7E,CAAC,GAAG,UAAU,IAEd,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GACjD,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAAC,GAC3B,SAAS,CAAA;AAEf,MAAM,MAAM,wBAAwB,CAClC,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,IAE7E,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GACjD,oBAAoB,CAAC,MAAM,CAAC,GAC5B,SAAS,CAAA;AAEf,MAAM,MAAM,iBAAiB,CAC3B,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,EAC7E,CAAC,GAAG,UAAU,IAEd,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,CAAC,GAClD,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,GACxB,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,CAAC,GAC3C,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,GACxB,SAAS,CAAA;AAEjB,MAAM,MAAM,qBAAqB,CAC/B,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,EAC7E,CAAC,GAAG,UAAU,IAEd,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,CAAC,GAClD,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,GAC5B,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,CAAC,GAC3C,gBAAgB,CAAC,OAAO,EAAE,CAAC,CAAC,GAC5B,SAAS,CAAA;AAEjB,MAAM,MAAM,yBAAyB,CACnC,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,IAE7E,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,CAAC,GAClD,oBAAoB,CAAC,OAAO,CAAC,GAC7B,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,OAAO,EAAE,GAAG,CAAC,GAC3C,oBAAoB,CAAC,OAAO,CAAC,GAC7B,SAAS,CAAA;AAEjB,MAAM,MAAM,kBAAkB,CAC5B,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,IAE7E,CAAC,SAAS,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,QAAQ,EAAE,GAAG,CAAC,GACjD,WAAW,CAAC,QAAQ,CAAC,GACrB,SAAS,CAAA;AAEf,MAAM,MAAM,gBAAgB,CAE1B,CAAC,SAAS,SAAS,GAAG,KAAK,GAAG,YAAY,GAAG,SAAS,GAAG,KAAK,GAAG,YAAY,IAC3E,CAAC,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC,MAAM,CAAC,SAAS,MAAM,CAAC,EAAE,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAA;AAEzE;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;EAKE,CAAA;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,KAAK,CAAC,KAAK,CAAC,UAAU,SAAS,kBAAkB,EAC/D,SAAS,EAAE,UAAU,GACpB,QAAQ,UAAU,EAAE,CAAA;AACvB,wBAAgB,KAAK,CACnB,KAAK,CAAC,UAAU,SAAS,kBAAkB,EAC3C,KAAK,CAAC,WAAW,SAAS,UAAU,EACpC,KAAK,CAAC,UAAU,SAAS,cAAc,EAEvC,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,UAAU,SAAS,IAAI,GAAG,GAAG,GAAG,KAAK,GAAG,UAAU,GACvD,QAAQ,UAAU,IAAI,WAAW,IAAI,UAAU,EAAE,CAAA;AACpD,wBAAgB,KAAK,CACnB,KAAK,CAAC,UAAU,SAAS,kBAAkB,EAC3C,KAAK,CAAC,OAAO,SAAS,YAAY,EAElC,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,SAAS,WAAW,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,KAAK,GACzE,QAAQ,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE,CAAA;AACtE,wBAAgB,KAAK,CACnB,KAAK,CAAC,UAAU,SAAS,kBAAkB,EAC3C,KAAK,CAAC,OAAO,SAAS,YAAY,EAClC,KAAK,CAAC,UAAU,SAAS,cAAc,CAAC,OAAO,CAAC,EAEhD,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,EACrB,IAAI,EAAE,UAAU,SAAS,IAAI,GAAG,GAAG,GAAG,KAAK,GAAG,UAAU,GACvD,QAAQ,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,UAAU,EAAE,CAAA"}
package/dist/helpers.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { assertAtIdentifierString, assertStringFormat, } from './core.js';
1
2
  import { object, optional, regexp, string, } from './schema.js';
2
3
  export function getMain(ns) {
3
4
  return 'main' in ns ? ns.main : ns;
@@ -11,4 +12,36 @@ export const lexErrorDataSchema = object({
11
12
  // description of the error, appropriate for display to humans
12
13
  message: optional(string()),
13
14
  });
15
+ export function atUri(authority, record, rkey) {
16
+ /**
17
+ * @NOTE because we are encoding potentially untrusted input into a URI, we
18
+ * validate the input against the AT Protocol constraints, ensuring that no
19
+ * invalid URIs can be generated.
20
+ */
21
+ switch (typeof record) {
22
+ case 'undefined': {
23
+ assertAtIdentifierString(authority);
24
+ return `at://${authority}`;
25
+ }
26
+ case 'string': {
27
+ if (!rkey) {
28
+ throw new TypeError('Record key is required when record is a string');
29
+ }
30
+ assertAtIdentifierString(authority);
31
+ assertStringFormat(record, 'nsid');
32
+ assertStringFormat(rkey, 'record-key');
33
+ return `at://${authority}/${record}/${rkey}`;
34
+ }
35
+ default: {
36
+ // @NOTE The use of a schema assumes that the collection ($type) is a
37
+ // valid NSID that can safely be included in the URI without additional
38
+ // checks.
39
+ assertAtIdentifierString(authority);
40
+ const schema = getMain(record);
41
+ // @NOTE parsing will apply defaults, so that literal keys will be
42
+ // properly validated and included in the URI.
43
+ return `at://${authority}/${schema.$type}/${schema.keySchema.parse(rkey)}`;
44
+ }
45
+ }
46
+ }
14
47
  //# sourceMappingURL=helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EAOL,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,GACP,MAAM,aAAa,CAAA;AAIpB,MAAM,UAAU,OAAO,CAAmB,EAAW;IACnD,OAAO,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;AACpC,CAAC;AAuFD;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;IACvC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC,WAAW,EAAE,4CAA4C,CAAC;IACxE,8DAA8D;IAC9D,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;CAC5B,CAAgC,CAAA","sourcesContent":["import { LexErrorData } from '@atproto/lex-data'\nimport { InferOutput, Restricted, Schema } from './core.js'\nimport {\n InferPayload,\n InferPayloadBody,\n InferPayloadEncoding,\n Procedure,\n Query,\n Subscription,\n object,\n optional,\n regexp,\n string,\n} from './schema.js'\n\nexport type Main<T> = T | { main: T }\n\nexport function getMain<T extends object>(ns: Main<T>): T {\n return 'main' in ns ? ns.main : ns\n}\n\n/**\n * Every XRPC implementation should translate `application/json` and `text/*`\n * payloads into their native equivalent ({@link LexValue} or string). Binary\n * data payloads, however, can be represented differently depending on the\n * environment and use case (e.g. `Uint8Array`, `Blob`, `Buffer`,\n * `ReadableStream`, etc.). This type is a placeholder to represent binary data\n * when not explicitly provided.\n */\nexport type BinaryData = Restricted<'Binary data'>\n\nexport type InferMethodParams<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Procedure<any, infer TParams, any, any, any>\n ? InferOutput<TParams>\n : M extends Query<any, infer TParams, any, any>\n ? InferOutput<TParams>\n : M extends Subscription<any, infer TParams, any, any>\n ? InferOutput<TParams>\n : never\n\nexport type InferMethodInput<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, infer TInput, any, any>\n ? InferPayload<TInput, B>\n : undefined\n\nexport type InferMethodInputBody<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, infer TInput, any, any>\n ? InferPayloadBody<TInput, B>\n : undefined\n\nexport type InferMethodInputEncoding<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Procedure<any, any, infer TInput, any, any>\n ? InferPayloadEncoding<TInput>\n : undefined\n\nexport type InferMethodOutput<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, any, infer TOutput, any>\n ? InferPayload<TOutput, B>\n : M extends Query<any, any, infer TOutput, any>\n ? InferPayload<TOutput, B>\n : undefined\n\nexport type InferMethodOutputBody<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, any, infer TOutput, any>\n ? InferPayloadBody<TOutput, B>\n : M extends Query<any, any, infer TOutput, any>\n ? InferPayloadBody<TOutput, B>\n : undefined\n\nexport type InferMethodOutputEncoding<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Procedure<any, any, any, infer TOutput, any>\n ? InferPayloadEncoding<TOutput>\n : M extends Query<any, any, infer TOutput, any>\n ? InferPayloadEncoding<TOutput>\n : undefined\n\nexport type InferMethodMessage<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Subscription<any, any, infer TMessage, any>\n ? InferOutput<TMessage>\n : undefined\n\nexport type InferMethodError<\n //\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> = M extends { errors: readonly (infer E extends string)[] } ? E : never\n\n/**\n * @see {@link https://atproto.com/specs/xrpc#error-responses}\n */\nexport const lexErrorDataSchema = object({\n // type name of the error (generic ASCII constant, no whitespace)\n error: regexp(/^[\\w_-]+$/, 'Expected ASCII constant with no whitespace'),\n // description of the error, appropriate for display to humans\n message: optional(string()),\n}) satisfies Schema<LexErrorData>\n"]}
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,WAAW,CAAA;AAClB,OAAO,EASL,MAAM,EACN,QAAQ,EACR,MAAM,EACN,MAAM,GACP,MAAM,aAAa,CAAA;AAIpB,MAAM,UAAU,OAAO,CAAmB,EAAW;IACnD,OAAO,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;AACpC,CAAC;AAuFD;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAC;IACvC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC,WAAW,EAAE,4CAA4C,CAAC;IACxE,8DAA8D;IAC9D,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;CAC5B,CAAgC,CAAA;AA2DjC,MAAM,UAAU,KAAK,CACnB,SAA6B,EAC7B,MAAoC,EACpC,IAAa;IAEb;;;;OAIG;IACH,QAAQ,OAAO,MAAM,EAAE,CAAC;QACtB,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,wBAAwB,CAAC,SAAS,CAAC,CAAA;YACnC,OAAO,QAAQ,SAAS,EAAE,CAAA;QAC5B,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,SAAS,CAAC,gDAAgD,CAAC,CAAA;YACvE,CAAC;YACD,wBAAwB,CAAC,SAAS,CAAC,CAAA;YACnC,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAClC,kBAAkB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;YACtC,OAAO,QAAQ,SAAS,IAAI,MAAM,IAAI,IAAI,EAAE,CAAA;QAC9C,CAAC;QAED,OAAO,CAAC,CAAC,CAAC;YACR,qEAAqE;YACrE,uEAAuE;YACvE,UAAU;YACV,wBAAwB,CAAC,SAAS,CAAC,CAAA;YACnC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;YAC9B,kEAAkE;YAClE,8CAA8C;YAC9C,OAAO,QAAQ,SAAS,IAAI,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAA;QAC5E,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["import { LexErrorData } from '@atproto/lex-data'\nimport {\n AtIdentifierString,\n InferOutput,\n NsidString,\n RecordKeyValue,\n Restricted,\n Schema,\n assertAtIdentifierString,\n assertStringFormat,\n} from './core.js'\nimport {\n InferPayload,\n InferPayloadBody,\n InferPayloadEncoding,\n InferRecordKey,\n Procedure,\n Query,\n RecordSchema,\n Subscription,\n object,\n optional,\n regexp,\n string,\n} from './schema.js'\n\nexport type Main<T> = T | { main: T }\n\nexport function getMain<T extends object>(ns: Main<T>): T {\n return 'main' in ns ? ns.main : ns\n}\n\n/**\n * Every XRPC implementation should translate `application/json` and `text/*`\n * payloads into their native equivalent ({@link LexValue} or string). Binary\n * data payloads, however, can be represented differently depending on the\n * environment and use case (e.g. `Uint8Array`, `Blob`, `Buffer`,\n * `ReadableStream`, etc.). This type is a placeholder to represent binary data\n * when not explicitly provided.\n */\nexport type BinaryData = Restricted<'Binary data'>\n\nexport type InferMethodParams<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Procedure<any, infer TParams, any, any, any>\n ? InferOutput<TParams>\n : M extends Query<any, infer TParams, any, any>\n ? InferOutput<TParams>\n : M extends Subscription<any, infer TParams, any, any>\n ? InferOutput<TParams>\n : never\n\nexport type InferMethodInput<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, infer TInput, any, any>\n ? InferPayload<TInput, B>\n : undefined\n\nexport type InferMethodInputBody<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, infer TInput, any, any>\n ? InferPayloadBody<TInput, B>\n : undefined\n\nexport type InferMethodInputEncoding<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Procedure<any, any, infer TInput, any, any>\n ? InferPayloadEncoding<TInput>\n : undefined\n\nexport type InferMethodOutput<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, any, infer TOutput, any>\n ? InferPayload<TOutput, B>\n : M extends Query<any, any, infer TOutput, any>\n ? InferPayload<TOutput, B>\n : undefined\n\nexport type InferMethodOutputBody<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n B = BinaryData,\n> =\n M extends Procedure<any, any, any, infer TOutput, any>\n ? InferPayloadBody<TOutput, B>\n : M extends Query<any, any, infer TOutput, any>\n ? InferPayloadBody<TOutput, B>\n : undefined\n\nexport type InferMethodOutputEncoding<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Procedure<any, any, any, infer TOutput, any>\n ? InferPayloadEncoding<TOutput>\n : M extends Query<any, any, infer TOutput, any>\n ? InferPayloadEncoding<TOutput>\n : undefined\n\nexport type InferMethodMessage<\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> =\n M extends Subscription<any, any, infer TMessage, any>\n ? InferOutput<TMessage>\n : undefined\n\nexport type InferMethodError<\n //\n M extends Procedure | Query | Subscription = Procedure | Query | Subscription,\n> = M extends { errors: readonly (infer E extends string)[] } ? E : never\n\n/**\n * @see {@link https://atproto.com/specs/xrpc#error-responses}\n */\nexport const lexErrorDataSchema = object({\n // type name of the error (generic ASCII constant, no whitespace)\n error: regexp(/^[\\w_-]+$/, 'Expected ASCII constant with no whitespace'),\n // description of the error, appropriate for display to humans\n message: optional(string()),\n}) satisfies Schema<LexErrorData>\n\n/**\n * Helper function to construct AT Protocol URIs with compile-time & runtime\n * validation of their components. This function supports different use cases,\n * including constructing URIs from raw strings or from RecordSchema instances,\n * ensuring that the resulting URI adheres to the expected format.\n *\n * @throws {TypeError} If the arguments do not match the interface\n * @throws {Error} If AT-URI components are invalid\n *\n * @example\n * ```typescript\n * import { atUri } from '@atproto/lex'\n * import { app } from '#/lexicons/index.js'\n *\n * // Constructing a URI from raw components\n * const uri1 = atUri('did:example:123', 'app.bsky.feed.post', 'my-post')\n *\n * // Constructing a URI from a RecordSchema instance\n * const uri2 = atUri('did:example:123', app.bsky.feed.post, 'my-post')\n *\n * // Literal rkey can be omitted\n * const uri3 = atUri('did:example:123', app.bsky.actor.profile) // rkey 'self' is implied\n *\n * // Invalid URIs will throw errors\n * atUri('invalid authority', 'app.bsky.feed.post', 'my-post') // throws\n * atUri('did:example:123', 'invalid collection', 'my-post') // throws\n * atUri('did:example:123', 'app.bsky.feed.post', '..') // throws\n * ```\n */\nexport function atUri<const TAuthority extends AtIdentifierString>(\n authority: TAuthority,\n): `at://${TAuthority}`\nexport function atUri<\n const TAuthority extends AtIdentifierString,\n const TCollection extends NsidString,\n const TRecordKey extends RecordKeyValue,\n>(\n authority: TAuthority,\n nsid: TCollection,\n rkey: TRecordKey extends '..' | '.' ? never : TRecordKey,\n): `at://${TAuthority}/${TCollection}/${TRecordKey}`\nexport function atUri<\n const TAuthority extends AtIdentifierString,\n const TRecord extends RecordSchema,\n>(\n authority: TAuthority,\n record: TRecord['key'] extends `literal:${string}` ? Main<TRecord> : never,\n): `at://${TAuthority}/${TRecord['$type']}/${InferRecordKey<TRecord>}`\nexport function atUri<\n const TAuthority extends AtIdentifierString,\n const TRecord extends RecordSchema,\n const TRecordKey extends InferRecordKey<TRecord>,\n>(\n authority: TAuthority,\n record: Main<TRecord>,\n rkey: TRecordKey extends '..' | '.' ? never : TRecordKey,\n): `at://${TAuthority}/${TRecord['$type']}/${TRecordKey}`\nexport function atUri(\n authority: AtIdentifierString,\n record?: string | Main<RecordSchema>,\n rkey?: string,\n) {\n /**\n * @NOTE because we are encoding potentially untrusted input into a URI, we\n * validate the input against the AT Protocol constraints, ensuring that no\n * invalid URIs can be generated.\n */\n switch (typeof record) {\n case 'undefined': {\n assertAtIdentifierString(authority)\n return `at://${authority}`\n }\n\n case 'string': {\n if (!rkey) {\n throw new TypeError('Record key is required when record is a string')\n }\n assertAtIdentifierString(authority)\n assertStringFormat(record, 'nsid')\n assertStringFormat(rkey, 'record-key')\n return `at://${authority}/${record}/${rkey}`\n }\n\n default: {\n // @NOTE The use of a schema assumes that the collection ($type) is a\n // valid NSID that can safely be included in the URI without additional\n // checks.\n assertAtIdentifierString(authority)\n const schema = getMain(record)\n // @NOTE parsing will apply defaults, so that literal keys will be\n // properly validated and included in the URI.\n return `at://${authority}/${schema.$type}/${schema.keySchema.parse(rkey)}`\n }\n }\n}\n"]}
@@ -1,11 +1,11 @@
1
1
  import { LexMap } from '@atproto/lex-data';
2
- import { $Typed, InferInput, InferOutput, LexiconRecordKey, NsidString, Schema, TidString, Unknown$TypedObject, ValidationContext, Validator } from '../core.js';
2
+ import { $Typed, InferInput, InferOutput, LexiconRecordKey, NsidString, RecordKeyValue, Schema, Unknown$TypedObject, ValidationContext, Validator } from '../core.js';
3
3
  /**
4
4
  * Infers the record key type from a RecordSchema.
5
5
  *
6
6
  * @template R - The RecordSchema type
7
7
  */
8
- export type InferRecordKey<R extends RecordSchema> = R extends RecordSchema<infer TKey> ? RecordKeySchemaOutput<TKey> : never;
8
+ export type InferRecordKey<R extends RecordSchema> = R extends RecordSchema<infer TKey> ? RecordKeyValue<TKey> : never;
9
9
  export type TypedRecord<TType extends NsidString, TValue extends {
10
10
  $type?: unknown;
11
11
  } = {
@@ -57,8 +57,8 @@ export declare class RecordSchema<const TKey extends LexiconRecordKey = LexiconR
57
57
  */
58
58
  get $isTypeOf(): typeof this.isTypeOf;
59
59
  }
60
- export type RecordKeySchemaOutput<Key extends LexiconRecordKey> = Key extends 'any' ? string : Key extends 'tid' ? TidString : Key extends 'nsid' ? NsidString : Key extends `literal:${infer L extends string}` ? L : never;
61
- export type RecordKeySchema<Key extends LexiconRecordKey> = Schema<RecordKeySchemaOutput<Key>>;
60
+ export type RecordKeySchemaOutput<Key extends LexiconRecordKey> = RecordKeyValue<Key>;
61
+ export type RecordKeySchema<Key extends LexiconRecordKey> = Schema<RecordKeyValue<Key>>;
62
62
  /**
63
63
  * Ensures that a `$type` used in a record is a valid NSID (i.e. no fragment).
64
64
  */
@@ -1 +1 @@
1
- {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/schema/record.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/schema/record.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ import { $typed, Schema, } from '../core.js';
2
2
  import { lazyProperty } from '../util/lazy-property.js';
3
3
  import { literal } from './literal.js';
4
4
  import { string } from './string.js';
5
+ import { withDefault } from './with-default.js';
5
6
  /**
6
7
  * Schema for AT Protocol records with a type identifier and key constraints.
7
8
  *
@@ -62,10 +63,10 @@ export class RecordSchema extends Schema {
62
63
  return lazyProperty(this, '$isTypeOf', this.isTypeOf.bind(this));
63
64
  }
64
65
  }
65
- const keySchema = string({ minLength: 1 });
66
+ const keySchema = string({ format: 'record-key' });
66
67
  const tidSchema = string({ format: 'tid' });
67
68
  const nsidSchema = string({ format: 'nsid' });
68
- const selfLiteralSchema = literal('self');
69
+ const selfLiteralSchema = withDefault(literal('self'), 'self');
69
70
  function recordKey(key) {
70
71
  // @NOTE Use cached instances for common schemas
71
72
  if (key === 'any')
@@ -78,7 +79,7 @@ function recordKey(key) {
78
79
  const value = key.slice(8);
79
80
  if (value === 'self')
80
81
  return selfLiteralSchema;
81
- return literal(value);
82
+ return withDefault(literal(value), value);
82
83
  }
83
84
  throw new Error(`Unsupported record key type: ${key}`);
84
85
  }
@@ -1 +1 @@
1
- {"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/schema/record.ts"],"names":[],"mappings":"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","sourcesContent":["import { LexMap } from '@atproto/lex-data'\nimport {\n $Typed,\n $typed,\n InferInput,\n InferOutput,\n LexiconRecordKey,\n NsidString,\n Schema,\n TidString,\n Unknown$TypedObject,\n ValidationContext,\n Validator,\n} from '../core.js'\nimport { lazyProperty } from '../util/lazy-property.js'\nimport { literal } from './literal.js'\nimport { string } from './string.js'\n\n/**\n * Infers the record key type from a RecordSchema.\n *\n * @template R - The RecordSchema type\n */\nexport type InferRecordKey<R extends RecordSchema> =\n R extends RecordSchema<infer TKey> ? RecordKeySchemaOutput<TKey> : never\n\nexport type TypedRecord<\n TType extends NsidString,\n TValue extends { $type?: unknown } = { $type?: unknown },\n> = TValue extends { $type: TType }\n ? TValue\n : $Typed<Exclude<TValue, Unknown$TypedObject>, TType>\n\n/**\n * Schema for AT Protocol records with a type identifier and key constraints.\n *\n * Records are the primary data unit in AT Protocol. Each record has a `$type`\n * field identifying its Lexicon schema, and is stored at a specific key\n * (TID, NSID, or other format) in a repository.\n *\n * @template TKey - The record key type ('tid', 'nsid', 'any', or 'literal:...')\n * @template TType - The NSID string identifying this record type\n * @template TShape - The validator type for the record's data shape\n *\n * @example\n * ```ts\n * const postSchema = new RecordSchema(\n * 'tid',\n * 'app.bsky.feed.post',\n * l.object({ text: l.string(), createdAt: l.string() })\n * )\n * ```\n */\nexport class RecordSchema<\n const TKey extends LexiconRecordKey = LexiconRecordKey,\n const TType extends NsidString = NsidString,\n const TShape extends Validator<LexMap> = Validator<LexMap>,\n> extends Schema<\n $Typed<InferInput<TShape>, TType>,\n $Typed<InferOutput<TShape>, TType>\n> {\n readonly type = 'record' as const\n\n keySchema: RecordKeySchema<TKey>\n\n constructor(\n readonly key: TKey,\n readonly $type: TType,\n readonly schema: TShape,\n ) {\n super()\n this.keySchema = recordKey(key)\n }\n\n validateInContext(input: unknown, ctx: ValidationContext) {\n const result = ctx.validate(input, this.schema)\n\n if (!result.success) {\n return result\n }\n\n if (result.value.$type !== this.$type) {\n return ctx.issueInvalidPropertyValue(result.value, '$type', [this.$type])\n }\n\n return result\n }\n\n build(\n input: Omit<InferOutput<TShape>, '$type'>,\n ): $Typed<InferOutput<TShape>, TType>\n build(\n input: Omit<InferInput<TShape>, '$type'>,\n ): $Typed<InferInput<TShape>, TType>\n build(input: Record<string, unknown>) {\n return $typed(input, this.$type)\n }\n\n isTypeOf<TValue extends { $type?: unknown }>(\n value: TValue,\n ): value is TypedRecord<TType, TValue> {\n return value.$type === this.$type\n }\n\n /**\n * Bound alias for {@link build} for compatibility with generated utilities.\n * @see {@link build}\n */\n get $build(): typeof this.build {\n return lazyProperty(this, '$build', this.build.bind(this))\n }\n\n /**\n * Bound alias for {@link isTypeOf} for compatibility with generated utilities.\n * @see {@link isTypeOf}\n */\n get $isTypeOf(): typeof this.isTypeOf {\n return lazyProperty(this, '$isTypeOf', this.isTypeOf.bind(this))\n }\n}\n\nexport type RecordKeySchemaOutput<Key extends LexiconRecordKey> =\n Key extends 'any'\n ? string\n : Key extends 'tid'\n ? TidString\n : Key extends 'nsid'\n ? NsidString\n : Key extends `literal:${infer L extends string}`\n ? L\n : never\n\nexport type RecordKeySchema<Key extends LexiconRecordKey> = Schema<\n RecordKeySchemaOutput<Key>\n>\n\nconst keySchema = string({ minLength: 1 })\nconst tidSchema = string({ format: 'tid' })\nconst nsidSchema = string({ format: 'nsid' })\nconst selfLiteralSchema = literal('self')\n\nfunction recordKey<Key extends LexiconRecordKey>(\n key: Key,\n): RecordKeySchema<Key> {\n // @NOTE Use cached instances for common schemas\n if (key === 'any') return keySchema as any\n if (key === 'tid') return tidSchema as any\n if (key === 'nsid') return nsidSchema as any\n if (key.startsWith('literal:')) {\n const value = key.slice(8) as RecordKeySchemaOutput<Key>\n if (value === 'self') return selfLiteralSchema as any\n return literal(value)\n }\n\n throw new Error(`Unsupported record key type: ${key}`)\n}\n\n/**\n * Ensures that a `$type` used in a record is a valid NSID (i.e. no fragment).\n */\ntype AsNsid<T> = T extends `${string}#${string}` ? never : T\n\n/**\n * Creates a record schema for AT Protocol records.\n *\n * Records are the primary data unit in AT Protocol repositories. They have\n * a `$type` field identifying their Lexicon schema, and are stored at keys\n * following a specific format (TID, NSID, etc.).\n *\n * This function offers two overloads:\n * - One that infers the output type from the provided arguments (does not\n * support circular references)\n * - One with an explicitly defined interface for use with codegen and\n * circular references\n *\n * @param key - The record key type: 'tid', 'nsid', 'any', or 'literal:value'\n * @param type - The NSID identifying this record type (e.g., 'app.bsky.feed.post')\n * @param validator - Schema validator for the record's properties\n * @returns A new {@link RecordSchema} instance\n *\n * @example\n * ```ts\n * // Post record with TID key\n * const postSchema = l.record('tid', 'app.bsky.feed.post', l.object({\n * text: l.string({ maxGraphemes: 300 }),\n * createdAt: l.string({ format: 'datetime' }),\n * reply: l.optional(l.object({\n * root: l.ref(() => strongRefSchema),\n * parent: l.ref(() => strongRefSchema),\n * })),\n * }))\n *\n * // Profile record with literal 'self' key\n * const profileSchema = l.record('literal:self', 'app.bsky.actor.profile', l.object({\n * displayName: l.optional(l.string({ maxGraphemes: 64 })),\n * description: l.optional(l.string({ maxGraphemes: 256 })),\n * avatar: l.optional(l.blob({ accept: ['image/*'] })),\n * }))\n *\n * // Build a record with automatic $type injection\n * const post = postSchema.build({ text: 'Hello!', createdAt: new Date().toISOString() })\n * ```\n */\nexport function record<\n const K extends LexiconRecordKey,\n const T extends NsidString,\n const S extends Validator<LexMap>,\n>(key: K, type: AsNsid<T>, validator: S): RecordSchema<K, T, S>\nexport function record<\n const K extends LexiconRecordKey,\n const V extends LexMap & { $type: NsidString },\n>(\n key: K,\n type: AsNsid<V['$type']>,\n validator: Validator<Omit<V, '$type'>>,\n): RecordSchema<K, V['$type'], Validator<Omit<V, '$type'>>>\n/*@__NO_SIDE_EFFECTS__*/\nexport function record<\n const K extends LexiconRecordKey,\n const T extends NsidString,\n const S extends Validator<LexMap>,\n>(key: K, type: T, validator: S) {\n return new RecordSchema<K, T, S>(key, type, validator)\n}\n"]}
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/schema/record.ts"],"names":[],"mappings":"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","sourcesContent":["import { LexMap } from '@atproto/lex-data'\nimport {\n $Typed,\n $typed,\n InferInput,\n InferOutput,\n LexiconRecordKey,\n NsidString,\n RecordKeyValue,\n Schema,\n Unknown$TypedObject,\n ValidationContext,\n Validator,\n} from '../core.js'\nimport { lazyProperty } from '../util/lazy-property.js'\nimport { literal } from './literal.js'\nimport { string } from './string.js'\nimport { withDefault } from './with-default.js'\n\n/**\n * Infers the record key type from a RecordSchema.\n *\n * @template R - The RecordSchema type\n */\nexport type InferRecordKey<R extends RecordSchema> =\n R extends RecordSchema<infer TKey> ? RecordKeyValue<TKey> : never\n\nexport type TypedRecord<\n TType extends NsidString,\n TValue extends { $type?: unknown } = { $type?: unknown },\n> = TValue extends { $type: TType }\n ? TValue\n : $Typed<Exclude<TValue, Unknown$TypedObject>, TType>\n\n/**\n * Schema for AT Protocol records with a type identifier and key constraints.\n *\n * Records are the primary data unit in AT Protocol. Each record has a `$type`\n * field identifying its Lexicon schema, and is stored at a specific key\n * (TID, NSID, or other format) in a repository.\n *\n * @template TKey - The record key type ('tid', 'nsid', 'any', or 'literal:...')\n * @template TType - The NSID string identifying this record type\n * @template TShape - The validator type for the record's data shape\n *\n * @example\n * ```ts\n * const postSchema = new RecordSchema(\n * 'tid',\n * 'app.bsky.feed.post',\n * l.object({ text: l.string(), createdAt: l.string() })\n * )\n * ```\n */\nexport class RecordSchema<\n const TKey extends LexiconRecordKey = LexiconRecordKey,\n const TType extends NsidString = NsidString,\n const TShape extends Validator<LexMap> = Validator<LexMap>,\n> extends Schema<\n $Typed<InferInput<TShape>, TType>,\n $Typed<InferOutput<TShape>, TType>\n> {\n readonly type = 'record' as const\n\n keySchema: RecordKeySchema<TKey>\n\n constructor(\n readonly key: TKey,\n readonly $type: TType,\n readonly schema: TShape,\n ) {\n super()\n this.keySchema = recordKey(key)\n }\n\n validateInContext(input: unknown, ctx: ValidationContext) {\n const result = ctx.validate(input, this.schema)\n\n if (!result.success) {\n return result\n }\n\n if (result.value.$type !== this.$type) {\n return ctx.issueInvalidPropertyValue(result.value, '$type', [this.$type])\n }\n\n return result\n }\n\n build(\n input: Omit<InferOutput<TShape>, '$type'>,\n ): $Typed<InferOutput<TShape>, TType>\n build(\n input: Omit<InferInput<TShape>, '$type'>,\n ): $Typed<InferInput<TShape>, TType>\n build(input: Record<string, unknown>) {\n return $typed(input, this.$type)\n }\n\n isTypeOf<TValue extends { $type?: unknown }>(\n value: TValue,\n ): value is TypedRecord<TType, TValue> {\n return value.$type === this.$type\n }\n\n /**\n * Bound alias for {@link build} for compatibility with generated utilities.\n * @see {@link build}\n */\n get $build(): typeof this.build {\n return lazyProperty(this, '$build', this.build.bind(this))\n }\n\n /**\n * Bound alias for {@link isTypeOf} for compatibility with generated utilities.\n * @see {@link isTypeOf}\n */\n get $isTypeOf(): typeof this.isTypeOf {\n return lazyProperty(this, '$isTypeOf', this.isTypeOf.bind(this))\n }\n}\n\nexport type RecordKeySchemaOutput<Key extends LexiconRecordKey> =\n RecordKeyValue<Key>\n\nexport type RecordKeySchema<Key extends LexiconRecordKey> = Schema<\n RecordKeyValue<Key>\n>\n\nconst keySchema = string({ format: 'record-key' })\nconst tidSchema = string({ format: 'tid' })\nconst nsidSchema = string({ format: 'nsid' })\nconst selfLiteralSchema = withDefault(literal('self'), 'self')\n\nfunction recordKey<Key extends LexiconRecordKey>(\n key: Key,\n): RecordKeySchema<Key> {\n // @NOTE Use cached instances for common schemas\n if (key === 'any') return keySchema as any\n if (key === 'tid') return tidSchema as any\n if (key === 'nsid') return nsidSchema as any\n if (key.startsWith('literal:')) {\n const value = key.slice(8) as RecordKeyValue<Key>\n if (value === 'self') return selfLiteralSchema as any\n return withDefault(literal(value), value)\n }\n\n throw new Error(`Unsupported record key type: ${key}`)\n}\n\n/**\n * Ensures that a `$type` used in a record is a valid NSID (i.e. no fragment).\n */\ntype AsNsid<T> = T extends `${string}#${string}` ? never : T\n\n/**\n * Creates a record schema for AT Protocol records.\n *\n * Records are the primary data unit in AT Protocol repositories. They have\n * a `$type` field identifying their Lexicon schema, and are stored at keys\n * following a specific format (TID, NSID, etc.).\n *\n * This function offers two overloads:\n * - One that infers the output type from the provided arguments (does not\n * support circular references)\n * - One with an explicitly defined interface for use with codegen and\n * circular references\n *\n * @param key - The record key type: 'tid', 'nsid', 'any', or 'literal:value'\n * @param type - The NSID identifying this record type (e.g., 'app.bsky.feed.post')\n * @param validator - Schema validator for the record's properties\n * @returns A new {@link RecordSchema} instance\n *\n * @example\n * ```ts\n * // Post record with TID key\n * const postSchema = l.record('tid', 'app.bsky.feed.post', l.object({\n * text: l.string({ maxGraphemes: 300 }),\n * createdAt: l.string({ format: 'datetime' }),\n * reply: l.optional(l.object({\n * root: l.ref(() => strongRefSchema),\n * parent: l.ref(() => strongRefSchema),\n * })),\n * }))\n *\n * // Profile record with literal 'self' key\n * const profileSchema = l.record('literal:self', 'app.bsky.actor.profile', l.object({\n * displayName: l.optional(l.string({ maxGraphemes: 64 })),\n * description: l.optional(l.string({ maxGraphemes: 256 })),\n * avatar: l.optional(l.blob({ accept: ['image/*'] })),\n * }))\n *\n * // Build a record with automatic $type injection\n * const post = postSchema.build({ text: 'Hello!', createdAt: new Date().toISOString() })\n * ```\n */\nexport function record<\n const K extends LexiconRecordKey,\n const T extends NsidString,\n const S extends Validator<LexMap>,\n>(key: K, type: AsNsid<T>, validator: S): RecordSchema<K, T, S>\nexport function record<\n const K extends LexiconRecordKey,\n const V extends LexMap & { $type: NsidString },\n>(\n key: K,\n type: AsNsid<V['$type']>,\n validator: Validator<Omit<V, '$type'>>,\n): RecordSchema<K, V['$type'], Validator<Omit<V, '$type'>>>\n/*@__NO_SIDE_EFFECTS__*/\nexport function record<\n const K extends LexiconRecordKey,\n const T extends NsidString,\n const S extends Validator<LexMap>,\n>(key: K, type: T, validator: S) {\n return new RecordSchema<K, T, S>(key, type, validator)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/lex-schema",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -1,4 +1,4 @@
1
- import { isValidRecordKey } from '@atproto/syntax'
1
+ import { NsidString, TidString, isValidRecordKey } from '@atproto/syntax'
2
2
 
3
3
  /**
4
4
  * The valid record key constraint types in a lexicon definition.
@@ -64,3 +64,22 @@ export function asLexiconRecordKey(key: unknown): LexiconRecordKey {
64
64
  if (isLexiconRecordKey(key)) return key
65
65
  throw new Error(`Invalid record key: ${String(key)}`)
66
66
  }
67
+
68
+ /**
69
+ * Maps a lexicon record key definition to its corresponding string subtype.
70
+ *
71
+ * - `'any'` maps to `string`
72
+ * - `'nsid'` maps to `NsidString`
73
+ * - `'tid'` maps to `TidString`
74
+ * - `'literal:...'` maps to the literal string value
75
+ */
76
+ export type RecordKeyValue<Key extends LexiconRecordKey = LexiconRecordKey> =
77
+ Key extends 'any'
78
+ ? string
79
+ : Key extends 'tid'
80
+ ? TidString
81
+ : Key extends 'nsid'
82
+ ? NsidString
83
+ : Key extends `literal:${infer L extends string}`
84
+ ? L
85
+ : never
@@ -1,4 +1,4 @@
1
- import { describe, test } from 'vitest'
1
+ import { describe, expect, expectTypeOf, it, test } from 'vitest'
2
2
  import * as l from './external.js'
3
3
 
4
4
  class BinaryValue {
@@ -567,3 +567,128 @@ describe('InferMethodMessage', () => {
567
567
  })
568
568
  })
569
569
  })
570
+
571
+ describe(l.atUri, () => {
572
+ describe('string collection', () => {
573
+ const did = 'did:example:alice'
574
+
575
+ it('builds a valid record URI', () => {
576
+ const uri = l.atUri(did, 'com.ex.foo', 'bar')
577
+ expect(uri).toBe('at://did:example:alice/com.ex.foo/bar')
578
+ expectTypeOf(uri).toEqualTypeOf<`at://did:example:alice/com.ex.foo/bar`>()
579
+ })
580
+
581
+ it('validates record key values', () => {
582
+ expect(() => {
583
+ // @ts-expect-error
584
+ l.atUri(did, 'com.ex.foo', '.')
585
+ }).toThrow()
586
+ expect(() => {
587
+ // @ts-expect-error
588
+ l.atUri(did, 'com.ex.foo', '..')
589
+ }).toThrow()
590
+ })
591
+
592
+ it('expects a second argument', () => {
593
+ expect(() => {
594
+ // @ts-expect-error
595
+ l.atUri(did, 'com.ex.foo')
596
+ }).toThrow()
597
+ })
598
+ })
599
+
600
+ describe('literal', () => {
601
+ const schema = l.record('literal:bar', 'com.ex.foo', l.object({}))
602
+ const did = 'did:example:alice'
603
+
604
+ it('allows omitting the record key for literal keys', () => {
605
+ const uri = l.atUri(did, schema)
606
+ expect(uri).toBe('at://did:example:alice/com.ex.foo/bar')
607
+ expectTypeOf(uri).toEqualTypeOf<`at://did:example:alice/com.ex.foo/bar`>()
608
+ })
609
+
610
+ it('still allows providing the record key for literal keys', () => {
611
+ expectTypeOf(
612
+ l.atUri(did, schema, 'bar'),
613
+ ).toEqualTypeOf<`at://did:example:alice/com.ex.foo/bar`>()
614
+ })
615
+
616
+ it('validates invalid record keys', () => {
617
+ expect(() => {
618
+ // @ts-expect-error
619
+ l.atUri(did, schema, 'wrong')
620
+ }).toThrow()
621
+ })
622
+ })
623
+
624
+ describe('tid', () => {
625
+ const schema = l.record('tid', 'com.ex.foo', l.object({}))
626
+ const did = 'did:example:alice'
627
+
628
+ it('builds a valid record URI', () => {
629
+ const uri = l.atUri(did, schema, '3jzfcijpj2z2a')
630
+ expectTypeOf(
631
+ uri,
632
+ ).toEqualTypeOf<`at://did:example:alice/com.ex.foo/3jzfcijpj2z2a`>()
633
+ expect(uri).toBe('at://did:example:alice/com.ex.foo/3jzfcijpj2z2a')
634
+ })
635
+
636
+ it('expects a second argument for non-literal keys', () => {
637
+ expect(() => {
638
+ // @ts-expect-error
639
+ l.atUri(did, schema)
640
+ }).toThrow()
641
+ })
642
+
643
+ it('validates the record key value', () => {
644
+ expect(() => {
645
+ l.atUri(did, schema, 'invalid-tid')
646
+ }).toThrow()
647
+ })
648
+ })
649
+
650
+ describe('any', () => {
651
+ const schema = l.record('any', 'com.ex.foo', l.object({}))
652
+ const did = 'did:example:alice'
653
+
654
+ it('builds a valid record URI valid keys', () => {
655
+ const uri = l.atUri(did, schema, 'customKey')
656
+ expect(uri).toBe('at://did:example:alice/com.ex.foo/customKey')
657
+ expectTypeOf(
658
+ uri,
659
+ ).toEqualTypeOf<`at://did:example:alice/com.ex.foo/customKey`>()
660
+ })
661
+
662
+ it('rejects invalid record key values', () => {
663
+ expect(() => {
664
+ // @ts-expect-error
665
+ l.atUri(did, schema, '.')
666
+ }).toThrow()
667
+ expect(() => {
668
+ // @ts-expect-error
669
+ l.atUri(did, schema, '..')
670
+ }).toThrow()
671
+ })
672
+
673
+ it('expects a second argument', () => {
674
+ expect(() => {
675
+ // @ts-expect-error
676
+ l.atUri(did, schema)
677
+ }).toThrow()
678
+ })
679
+
680
+ describe('edge cases', () => {
681
+ it('limits the record key to 512 characters', () => {
682
+ const uri = l.atUri(did, schema, 'a'.repeat(512))
683
+
684
+ expect(uri).toBe(`at://did:example:alice/com.ex.foo/${'a'.repeat(512)}`)
685
+ expectTypeOf(
686
+ uri,
687
+ ).toEqualTypeOf<`at://did:example:alice/com.ex.foo/${string}`>()
688
+ expect(() => {
689
+ l.atUri(did, schema, 'a'.repeat(513))
690
+ }).toThrow()
691
+ })
692
+ })
693
+ })
694
+ })
package/src/helpers.ts CHANGED
@@ -1,11 +1,22 @@
1
1
  import { LexErrorData } from '@atproto/lex-data'
2
- import { InferOutput, Restricted, Schema } from './core.js'
2
+ import {
3
+ AtIdentifierString,
4
+ InferOutput,
5
+ NsidString,
6
+ RecordKeyValue,
7
+ Restricted,
8
+ Schema,
9
+ assertAtIdentifierString,
10
+ assertStringFormat,
11
+ } from './core.js'
3
12
  import {
4
13
  InferPayload,
5
14
  InferPayloadBody,
6
15
  InferPayloadEncoding,
16
+ InferRecordKey,
7
17
  Procedure,
8
18
  Query,
19
+ RecordSchema,
9
20
  Subscription,
10
21
  object,
11
22
  optional,
@@ -113,3 +124,99 @@ export const lexErrorDataSchema = object({
113
124
  // description of the error, appropriate for display to humans
114
125
  message: optional(string()),
115
126
  }) satisfies Schema<LexErrorData>
127
+
128
+ /**
129
+ * Helper function to construct AT Protocol URIs with compile-time & runtime
130
+ * validation of their components. This function supports different use cases,
131
+ * including constructing URIs from raw strings or from RecordSchema instances,
132
+ * ensuring that the resulting URI adheres to the expected format.
133
+ *
134
+ * @throws {TypeError} If the arguments do not match the interface
135
+ * @throws {Error} If AT-URI components are invalid
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * import { atUri } from '@atproto/lex'
140
+ * import { app } from '#/lexicons/index.js'
141
+ *
142
+ * // Constructing a URI from raw components
143
+ * const uri1 = atUri('did:example:123', 'app.bsky.feed.post', 'my-post')
144
+ *
145
+ * // Constructing a URI from a RecordSchema instance
146
+ * const uri2 = atUri('did:example:123', app.bsky.feed.post, 'my-post')
147
+ *
148
+ * // Literal rkey can be omitted
149
+ * const uri3 = atUri('did:example:123', app.bsky.actor.profile) // rkey 'self' is implied
150
+ *
151
+ * // Invalid URIs will throw errors
152
+ * atUri('invalid authority', 'app.bsky.feed.post', 'my-post') // throws
153
+ * atUri('did:example:123', 'invalid collection', 'my-post') // throws
154
+ * atUri('did:example:123', 'app.bsky.feed.post', '..') // throws
155
+ * ```
156
+ */
157
+ export function atUri<const TAuthority extends AtIdentifierString>(
158
+ authority: TAuthority,
159
+ ): `at://${TAuthority}`
160
+ export function atUri<
161
+ const TAuthority extends AtIdentifierString,
162
+ const TCollection extends NsidString,
163
+ const TRecordKey extends RecordKeyValue,
164
+ >(
165
+ authority: TAuthority,
166
+ nsid: TCollection,
167
+ rkey: TRecordKey extends '..' | '.' ? never : TRecordKey,
168
+ ): `at://${TAuthority}/${TCollection}/${TRecordKey}`
169
+ export function atUri<
170
+ const TAuthority extends AtIdentifierString,
171
+ const TRecord extends RecordSchema,
172
+ >(
173
+ authority: TAuthority,
174
+ record: TRecord['key'] extends `literal:${string}` ? Main<TRecord> : never,
175
+ ): `at://${TAuthority}/${TRecord['$type']}/${InferRecordKey<TRecord>}`
176
+ export function atUri<
177
+ const TAuthority extends AtIdentifierString,
178
+ const TRecord extends RecordSchema,
179
+ const TRecordKey extends InferRecordKey<TRecord>,
180
+ >(
181
+ authority: TAuthority,
182
+ record: Main<TRecord>,
183
+ rkey: TRecordKey extends '..' | '.' ? never : TRecordKey,
184
+ ): `at://${TAuthority}/${TRecord['$type']}/${TRecordKey}`
185
+ export function atUri(
186
+ authority: AtIdentifierString,
187
+ record?: string | Main<RecordSchema>,
188
+ rkey?: string,
189
+ ) {
190
+ /**
191
+ * @NOTE because we are encoding potentially untrusted input into a URI, we
192
+ * validate the input against the AT Protocol constraints, ensuring that no
193
+ * invalid URIs can be generated.
194
+ */
195
+ switch (typeof record) {
196
+ case 'undefined': {
197
+ assertAtIdentifierString(authority)
198
+ return `at://${authority}`
199
+ }
200
+
201
+ case 'string': {
202
+ if (!rkey) {
203
+ throw new TypeError('Record key is required when record is a string')
204
+ }
205
+ assertAtIdentifierString(authority)
206
+ assertStringFormat(record, 'nsid')
207
+ assertStringFormat(rkey, 'record-key')
208
+ return `at://${authority}/${record}/${rkey}`
209
+ }
210
+
211
+ default: {
212
+ // @NOTE The use of a schema assumes that the collection ($type) is a
213
+ // valid NSID that can safely be included in the URI without additional
214
+ // checks.
215
+ assertAtIdentifierString(authority)
216
+ const schema = getMain(record)
217
+ // @NOTE parsing will apply defaults, so that literal keys will be
218
+ // properly validated and included in the URI.
219
+ return `at://${authority}/${schema.$type}/${schema.keySchema.parse(rkey)}`
220
+ }
221
+ }
222
+ }
@@ -12,7 +12,6 @@ describe('RecordSchema', () => {
12
12
  'any',
13
13
  'app.bsky.feed.post',
14
14
  object({
15
- $type: string(),
16
15
  text: string(),
17
16
  }),
18
17
  )
@@ -284,7 +283,6 @@ describe('RecordSchema', () => {
284
283
  'any',
285
284
  'app.bsky.feed.post',
286
285
  object({
287
- $type: string(),
288
286
  text: string(),
289
287
  }),
290
288
  )
@@ -320,7 +318,6 @@ describe('RecordSchema', () => {
320
318
  'tid',
321
319
  'app.bsky.feed.post',
322
320
  object({
323
- $type: string(),
324
321
  text: string(),
325
322
  }),
326
323
  )
@@ -356,7 +353,6 @@ describe('RecordSchema', () => {
356
353
  'nsid',
357
354
  'app.bsky.feed.post',
358
355
  object({
359
- $type: string(),
360
356
  text: string(),
361
357
  }),
362
358
  )
@@ -395,7 +391,6 @@ describe('RecordSchema', () => {
395
391
  'literal:self',
396
392
  'app.bsky.feed.post',
397
393
  object({
398
- $type: string(),
399
394
  text: string(),
400
395
  }),
401
396
  )
@@ -426,7 +421,6 @@ describe('RecordSchema', () => {
426
421
  'literal:customKey',
427
422
  'app.bsky.feed.post',
428
423
  object({
429
- $type: string(),
430
424
  text: string(),
431
425
  }),
432
426
  )
@@ -453,7 +447,6 @@ describe('RecordSchema', () => {
453
447
  'any',
454
448
  'app.bsky.feed.post#main',
455
449
  object({
456
- $type: string(),
457
450
  text: string(),
458
451
  }),
459
452
  )
@@ -488,7 +481,6 @@ describe('RecordSchema', () => {
488
481
  'any',
489
482
  'app.bsky.feed.post',
490
483
  object({
491
- $type: string(),
492
484
  text: string({ maxLength: 300 }),
493
485
  createdAt: string({ format: 'datetime' }),
494
486
  }),
@@ -527,7 +519,6 @@ describe('RecordSchema', () => {
527
519
  'any',
528
520
  'app.bsky.feed.post',
529
521
  object({
530
- $type: string(),
531
522
  text: string(),
532
523
  }),
533
524
  )
@@ -592,7 +583,6 @@ describe('RecordSchema', () => {
592
583
  'any',
593
584
  'app.bsky.complex',
594
585
  object({
595
- $type: string(),
596
586
  nested: object({
597
587
  deep: object({
598
588
  value: string(),
@@ -618,7 +608,6 @@ describe('RecordSchema', () => {
618
608
  'any',
619
609
  'app.bsky.feed.post',
620
610
  object({
621
- $type: string(),
622
611
  text: string(),
623
612
  }),
624
613
  )
@@ -639,7 +628,6 @@ describe('RecordSchema', () => {
639
628
  'any',
640
629
  'app.bsky.feed.post',
641
630
  object({
642
- $type: string(),
643
631
  text: string(),
644
632
  }),
645
633
  )
@@ -656,7 +644,6 @@ describe('RecordSchema', () => {
656
644
  'any',
657
645
  'app.bsky.feed.post',
658
646
  object({
659
- $type: string(),
660
647
  text: string(),
661
648
  author: string(),
662
649
  }),
@@ -682,35 +669,32 @@ describe('RecordSchema', () => {
682
669
 
683
670
  describe('different record key types', () => {
684
671
  it('constructs with key type "any"', () => {
685
- const schema = record('any', 'app.bsky.test', object({ $type: string() }))
672
+ const schema = record('any', 'app.bsky.test', object({}))
686
673
  expect(schema.key).toBe('any')
687
674
  expect(schema.keySchema).toBeDefined()
688
675
  })
689
676
 
690
677
  it('constructs with key type "tid"', () => {
691
- const schema = record('tid', 'app.bsky.test', object({ $type: string() }))
678
+ const schema = record('tid', 'app.bsky.test', object({}))
692
679
  expect(schema.key).toBe('tid')
693
680
  expect(schema.keySchema).toBeDefined()
694
681
  })
695
682
 
696
683
  it('constructs with key type "nsid"', () => {
697
- const schema = record(
698
- 'nsid',
699
- 'app.bsky.test',
700
- object({ $type: string() }),
701
- )
684
+ const schema = record('nsid', 'app.bsky.test', object({}))
702
685
  expect(schema.key).toBe('nsid')
703
686
  expect(schema.keySchema).toBeDefined()
687
+ expect(schema.keySchema.safeParse('app.bsky.post').success).toBe(true)
688
+ expect(schema.keySchema.safeParse('invalid-nsid').success).toBe(false)
704
689
  })
705
690
 
706
691
  it('constructs with literal key type', () => {
707
- const schema = record(
708
- 'literal:custom',
709
- 'app.bsky.test',
710
- object({ $type: string() }),
711
- )
692
+ const schema = record('literal:custom', 'app.bsky.test', object({}))
712
693
  expect(schema.key).toBe('literal:custom')
713
694
  expect(schema.keySchema).toBeDefined()
695
+ // Applies default value in parse mode
696
+ expect(schema.keySchema.parse(undefined)).toBe('custom')
697
+ expect(schema.keySchema.safeParse('not-custom').success).toBe(false)
714
698
  })
715
699
  })
716
700
 
@@ -719,7 +703,6 @@ describe('RecordSchema', () => {
719
703
  'any',
720
704
  'app.bsky.feed.post',
721
705
  object({
722
- $type: string(),
723
706
  text: string(),
724
707
  }),
725
708
  )
@@ -755,7 +738,6 @@ describe('RecordSchema', () => {
755
738
  'any',
756
739
  'app.bsky.feed.post',
757
740
  object({
758
- $type: string(),
759
741
  text: string(),
760
742
  }),
761
743
  )
@@ -774,7 +756,6 @@ describe('RecordSchema', () => {
774
756
  'any',
775
757
  'app.bsky.feed.post',
776
758
  object({
777
- $type: string(),
778
759
  text: string(),
779
760
  }),
780
761
  )
@@ -791,7 +772,6 @@ describe('RecordSchema', () => {
791
772
  'any',
792
773
  'app.bsky.feed.post#reply123',
793
774
  object({
794
- $type: string(),
795
775
  text: string(),
796
776
  }),
797
777
  )
@@ -809,7 +789,6 @@ describe('RecordSchema', () => {
809
789
  'any',
810
790
  'app.bsky.feed.post',
811
791
  object({
812
- $type: string(),
813
792
  text: string(),
814
793
  }),
815
794
  )
@@ -6,8 +6,8 @@ import {
6
6
  InferOutput,
7
7
  LexiconRecordKey,
8
8
  NsidString,
9
+ RecordKeyValue,
9
10
  Schema,
10
- TidString,
11
11
  Unknown$TypedObject,
12
12
  ValidationContext,
13
13
  Validator,
@@ -15,6 +15,7 @@ import {
15
15
  import { lazyProperty } from '../util/lazy-property.js'
16
16
  import { literal } from './literal.js'
17
17
  import { string } from './string.js'
18
+ import { withDefault } from './with-default.js'
18
19
 
19
20
  /**
20
21
  * Infers the record key type from a RecordSchema.
@@ -22,7 +23,7 @@ import { string } from './string.js'
22
23
  * @template R - The RecordSchema type
23
24
  */
24
25
  export type InferRecordKey<R extends RecordSchema> =
25
- R extends RecordSchema<infer TKey> ? RecordKeySchemaOutput<TKey> : never
26
+ R extends RecordSchema<infer TKey> ? RecordKeyValue<TKey> : never
26
27
 
27
28
  export type TypedRecord<
28
29
  TType extends NsidString,
@@ -120,24 +121,16 @@ export class RecordSchema<
120
121
  }
121
122
 
122
123
  export type RecordKeySchemaOutput<Key extends LexiconRecordKey> =
123
- Key extends 'any'
124
- ? string
125
- : Key extends 'tid'
126
- ? TidString
127
- : Key extends 'nsid'
128
- ? NsidString
129
- : Key extends `literal:${infer L extends string}`
130
- ? L
131
- : never
124
+ RecordKeyValue<Key>
132
125
 
133
126
  export type RecordKeySchema<Key extends LexiconRecordKey> = Schema<
134
- RecordKeySchemaOutput<Key>
127
+ RecordKeyValue<Key>
135
128
  >
136
129
 
137
- const keySchema = string({ minLength: 1 })
130
+ const keySchema = string({ format: 'record-key' })
138
131
  const tidSchema = string({ format: 'tid' })
139
132
  const nsidSchema = string({ format: 'nsid' })
140
- const selfLiteralSchema = literal('self')
133
+ const selfLiteralSchema = withDefault(literal('self'), 'self')
141
134
 
142
135
  function recordKey<Key extends LexiconRecordKey>(
143
136
  key: Key,
@@ -147,9 +140,9 @@ function recordKey<Key extends LexiconRecordKey>(
147
140
  if (key === 'tid') return tidSchema as any
148
141
  if (key === 'nsid') return nsidSchema as any
149
142
  if (key.startsWith('literal:')) {
150
- const value = key.slice(8) as RecordKeySchemaOutput<Key>
143
+ const value = key.slice(8) as RecordKeyValue<Key>
151
144
  if (value === 'self') return selfLiteralSchema as any
152
- return literal(value)
145
+ return withDefault(literal(value), value)
153
146
  }
154
147
 
155
148
  throw new Error(`Unsupported record key type: ${key}`)