@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 +6 -0
- package/dist/core/record-key.d.ts +10 -0
- package/dist/core/record-key.d.ts.map +1 -1
- package/dist/core/record-key.js.map +1 -1
- package/dist/helpers.d.ts +35 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +33 -0
- package/dist/helpers.js.map +1 -1
- package/dist/schema/record.d.ts +4 -4
- package/dist/schema/record.d.ts.map +1 -1
- package/dist/schema/record.js +4 -3
- package/dist/schema/record.js.map +1 -1
- package/package.json +1 -1
- package/src/core/record-key.ts +20 -1
- package/src/helpers.test.ts +126 -1
- package/src/helpers.ts +108 -1
- package/src/schema/record.test.ts +9 -30
- package/src/schema/record.ts +9 -16
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":"
|
|
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,
|
|
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
|
package/dist/helpers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
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
|
package/dist/helpers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"
|
|
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"]}
|
package/dist/schema/record.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { LexMap } from '@atproto/lex-data';
|
|
2
|
-
import { $Typed, InferInput, InferOutput, LexiconRecordKey, NsidString,
|
|
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> ?
|
|
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
|
|
61
|
-
export type RecordKeySchema<Key extends LexiconRecordKey> = Schema<
|
|
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":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,OAAO,EACL,MAAM,EAEN,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,MAAM,EACN,
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/schema/record.ts"],"names":[],"mappings":"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"}
|
package/dist/schema/record.js
CHANGED
|
@@ -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({
|
|
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":"AACA,OAAO,EAEL,MAAM,
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../../src/schema/record.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,MAAM,EAMN,MAAM,GAIP,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAiB/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,YAIX,SAAQ,MAGT;IAKC,YACW,GAAS,EACT,KAAY,EACZ,MAAc;QAEvB,KAAK,EAAE,CAAA;QAJE,QAAG,GAAH,GAAG,CAAM;QACT,UAAK,GAAL,KAAK,CAAO;QACZ,WAAM,GAAN,MAAM,CAAQ;QAPhB,SAAI,GAAG,QAAiB,CAAA;QAU/B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAA;IACjC,CAAC;IAED,iBAAiB,CAAC,KAAc,EAAE,GAAsB;QACtD,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAE/C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,MAAM,CAAA;QACf,CAAC;QAED,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,OAAO,GAAG,CAAC,yBAAyB,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;QAC3E,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC;IAQD,KAAK,CAAC,KAA8B;QAClC,OAAO,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IAClC,CAAC;IAED,QAAQ,CACN,KAAa;QAEb,OAAO,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,CAAA;IACnC,CAAC;IAED;;;OAGG;IACH,IAAI,MAAM;QACR,OAAO,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC5D,CAAC;IAED;;;OAGG;IACH,IAAI,SAAS;QACX,OAAO,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAClE,CAAC;CACF;AASD,MAAM,SAAS,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAA;AAClD,MAAM,SAAS,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;AAC3C,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAC7C,MAAM,iBAAiB,GAAG,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;AAE9D,SAAS,SAAS,CAChB,GAAQ;IAER,gDAAgD;IAChD,IAAI,GAAG,KAAK,KAAK;QAAE,OAAO,SAAgB,CAAA;IAC1C,IAAI,GAAG,KAAK,KAAK;QAAE,OAAO,SAAgB,CAAA;IAC1C,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,UAAiB,CAAA;IAC5C,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAwB,CAAA;QACjD,IAAI,KAAK,KAAK,MAAM;YAAE,OAAO,iBAAwB,CAAA;QACrD,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAA;IAC3C,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAA;AACxD,CAAC;AA6DD,wBAAwB;AACxB,MAAM,UAAU,MAAM,CAIpB,GAAM,EAAE,IAAO,EAAE,SAAY;IAC7B,OAAO,IAAI,YAAY,CAAU,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAA;AACxD,CAAC","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
package/src/core/record-key.ts
CHANGED
|
@@ -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
|
package/src/helpers.test.ts
CHANGED
|
@@ -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 {
|
|
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({
|
|
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({
|
|
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
|
)
|
package/src/schema/record.ts
CHANGED
|
@@ -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> ?
|
|
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
|
|
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
|
-
|
|
127
|
+
RecordKeyValue<Key>
|
|
135
128
|
>
|
|
136
129
|
|
|
137
|
-
const keySchema = string({
|
|
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
|
|
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}`)
|