lex-gql 0.1.0

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.
Files changed (4) hide show
  1. package/README.md +232 -0
  2. package/lex-gql.d.ts +172 -0
  3. package/lex-gql.js +1915 -0
  4. package/package.json +42 -0
package/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # lex-gql
2
+
3
+ GraphQL for AT Protocol Lexicons. Generates a fully-typed GraphQL schema from AT Protocol lexicon definitions with automatic join resolution.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install lex-gql graphql
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ import { parseLexicon, createAdapter } from 'lex-gql'
15
+
16
+ // Parse your lexicons
17
+ const lexicons = [
18
+ parseLexicon(profileLexiconJson),
19
+ parseLexicon(postLexiconJson),
20
+ parseLexicon(likeLexiconJson)
21
+ ]
22
+
23
+ // Create adapter with your data source
24
+ const adapter = createAdapter(lexicons, {
25
+ query: async (operation) => {
26
+ // Implement your data fetching logic
27
+ // operation.type: 'findMany' | 'findOne' | 'count' | 'aggregate' | 'create' | 'update' | 'delete'
28
+ // operation.collection: lexicon NSID
29
+ // operation.where: filter conditions
30
+ // operation.sort: sort clauses
31
+ // operation.pagination: { first, after, last, before }
32
+ // operation.select: requested field names (for query optimization)
33
+ return { rows: [...], hasNext: false, hasPrev: false, totalCount: 100 }
34
+ }
35
+ })
36
+
37
+ // Execute GraphQL queries
38
+ const result = await adapter.execute(`
39
+ query {
40
+ appBskyFeedPost(first: 10, where: { text: { contains: "hello" } }) {
41
+ edges {
42
+ node {
43
+ uri
44
+ text
45
+ appBskyActorProfileByDid {
46
+ displayName
47
+ }
48
+ }
49
+ }
50
+ pageInfo {
51
+ hasNextPage
52
+ }
53
+ }
54
+ }
55
+ `)
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - **Automatic schema generation** from AT Protocol lexicons
61
+ - **Relay-style pagination** with connections, edges, and pageInfo
62
+ - **Forward joins** via `*Resolved` fields for strongRef and at-uri references
63
+ - **Reverse joins** via `*Via*` fields (e.g., `appBskyFeedLikeViaSubject`)
64
+ - **DID joins** between collections via `*ByDid` fields
65
+ - **Filtering** with field conditions (eq, in, contains, gt, gte, lt, lte)
66
+ - **Sorting** with multi-field sort support
67
+ - **Aggregations** with groupBy support
68
+ - **Mutations** for create, update, delete operations
69
+ - **Batched join resolution** to avoid N+1 queries
70
+
71
+ ## API
72
+
73
+ ### `parseLexicon(json)`
74
+
75
+ Parse a raw lexicon JSON object into the internal format.
76
+
77
+ ```javascript
78
+ const lexicon = parseLexicon({
79
+ lexicon: 1,
80
+ id: 'app.bsky.feed.post',
81
+ defs: {
82
+ main: {
83
+ type: 'record',
84
+ record: {
85
+ type: 'object',
86
+ required: ['text', 'createdAt'],
87
+ properties: {
88
+ text: { type: 'string' },
89
+ createdAt: { type: 'string', format: 'datetime' }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ })
95
+ ```
96
+
97
+ ### `buildSchema(lexicons)`
98
+
99
+ Build a GraphQL schema from parsed lexicons (without resolvers).
100
+
101
+ ```javascript
102
+ import { buildSchema } from 'lex-gql'
103
+ import { printSchema } from 'graphql'
104
+
105
+ const schema = buildSchema(lexicons)
106
+ console.log(printSchema(schema))
107
+ ```
108
+
109
+ ### `createAdapter(lexicons, options)`
110
+
111
+ Create a full adapter with query execution.
112
+
113
+ ```javascript
114
+ const adapter = createAdapter(lexicons, {
115
+ query: async (operation) => {
116
+ // Your data source implementation
117
+ }
118
+ })
119
+
120
+ const { schema, execute } = adapter
121
+ ```
122
+
123
+ ### Utility Functions
124
+
125
+ ```javascript
126
+ import {
127
+ nsidToTypeName, // 'app.bsky.feed.post' -> 'AppBskyFeedPost'
128
+ nsidToFieldName, // 'app.bsky.feed.post' -> 'appBskyFeedPost'
129
+ nsidToCollectionName, // 'app.bsky.feed.post' -> 'post'
130
+ parseRefUri, // Parse ref URIs like 'app.bsky.feed.defs#postView'
131
+ refToTypeName, // Convert ref URI to GraphQL type name
132
+ mapLexiconType // Map lexicon types to GraphQL type names
133
+ } from 'lex-gql'
134
+ ```
135
+
136
+ ## Generated Schema Structure
137
+
138
+ For each record lexicon, lex-gql generates:
139
+
140
+ | Type | Example | Description |
141
+ |------|---------|-------------|
142
+ | Record type | `AppBskyFeedPost` | The main record with system and lexicon fields |
143
+ | Connection | `AppBskyFeedPostConnection` | Relay connection with edges and pageInfo |
144
+ | Edge | `AppBskyFeedPostEdge` | Edge with node and cursor |
145
+ | WhereInput | `AppBskyFeedPostWhereInput` | Filter input with field conditions |
146
+ | SortFieldInput | `AppBskyFeedPostSortFieldInput` | Sort input with field and direction |
147
+ | Input | `AppBskyFeedPostInput` | Mutation input type |
148
+ | Aggregated | `AppBskyFeedPostAggregated` | Aggregation result type |
149
+ | GroupByField | `AppBskyFeedPostGroupByField` | Enum for groupBy fields |
150
+ | FieldCondition | `AppBskyFeedPostFieldCondition` | Per-type field condition input |
151
+
152
+ ### System Fields
153
+
154
+ Every record type includes these system fields:
155
+
156
+ - `uri: String` - Record URI
157
+ - `cid: String` - Record CID
158
+ - `did: String` - DID of record author
159
+ - `collection: String` - Collection name
160
+ - `indexedAt: String` - When record was indexed
161
+ - `actorHandle: String` - Handle of the actor
162
+
163
+ ### Special Types
164
+
165
+ - `Blob` - Binary blob reference with `ref`, `mimeType`, `size`
166
+ - `ComAtprotoRepoStrongRef` - Strong reference with `cid`, `uri`
167
+ - `Record` - Union of all record types
168
+
169
+ ### Nested Types
170
+
171
+ AT Protocol lexicons can define helper types alongside their main type. These live in the `defs` section under names other than `main`:
172
+
173
+ ```json
174
+ {
175
+ "id": "app.bsky.richtext.facet",
176
+ "defs": {
177
+ "main": { ... },
178
+ "byteSlice": {
179
+ "type": "object",
180
+ "properties": {
181
+ "byteStart": { "type": "integer" },
182
+ "byteEnd": { "type": "integer" }
183
+ }
184
+ },
185
+ "mention": { ... },
186
+ "link": { ... }
187
+ }
188
+ }
189
+ ```
190
+
191
+ lex-gql generates GraphQL types for these with the pattern `{LexiconName}{DefName}`:
192
+
193
+ - `app.bsky.richtext.facet#byteSlice` → `AppBskyRichtextFacetByteSlice`
194
+ - `app.bsky.richtext.facet#mention` → `AppBskyRichtextFacetMention`
195
+ - `app.bsky.actor.defs#profileView` → `AppBskyActorDefsProfileView`
196
+
197
+ These types are included in the schema so they can be referenced by other types or used in queries.
198
+
199
+ ## Error Handling
200
+
201
+ ```javascript
202
+ import { LexGqlError, ErrorCodes } from 'lex-gql'
203
+
204
+ try {
205
+ parseLexicon(invalidJson)
206
+ } catch (err) {
207
+ if (err instanceof LexGqlError) {
208
+ console.log(err.code) // 'INVALID_LEXICON'
209
+ console.log(err.details) // { field: 'id' }
210
+ }
211
+ }
212
+
213
+ // Error codes:
214
+ // - INVALID_LEXICON
215
+ // - UNSUPPORTED_VERSION
216
+ // - QUERY_FAILED
217
+ // - VALIDATION_FAILED
218
+ ```
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ # Run tests
224
+ npm test
225
+
226
+ # Run tests in watch mode
227
+ npm run test:watch
228
+ ```
229
+
230
+ ## License
231
+
232
+ MIT
package/lex-gql.d.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Convert NSID to PascalCase type name
3
+ * @param {string} nsid - e.g. "app.bsky.feed.post"
4
+ * @returns {string} - e.g. "AppBskyFeedPost"
5
+ */
6
+ export function nsidToTypeName(nsid: string): string;
7
+ /**
8
+ * Convert NSID to camelCase field name
9
+ * @param {string} nsid - e.g. "app.bsky.feed.post"
10
+ * @returns {string} - e.g. "appBskyFeedPost"
11
+ */
12
+ export function nsidToFieldName(nsid: string): string;
13
+ /**
14
+ * Extract collection name (last segment) from NSID
15
+ * @param {string} nsid - e.g. "app.bsky.feed.post"
16
+ * @returns {string} - e.g. "post"
17
+ */
18
+ export function nsidToCollectionName(nsid: string): string;
19
+ /**
20
+ * Map AT Protocol lexicon type to GraphQL type name
21
+ * @param {string} lexiconType
22
+ * @returns {string}
23
+ */
24
+ export function mapLexiconType(lexiconType: string): string;
25
+ /**
26
+ * Parse a ref URI into nsid and fragment
27
+ * @param {string} refUri - e.g. "xyz.statusphere.post#embed" or "#mention"
28
+ * @returns {{ nsid: string|null, fragment: string }}
29
+ */
30
+ export function parseRefUri(refUri: string): {
31
+ nsid: string | null;
32
+ fragment: string;
33
+ };
34
+ /**
35
+ * Convert a ref URI to a GraphQL type name
36
+ * @param {string} refUri - e.g. "fm.teal.alpha.feed.defs#artist"
37
+ * @returns {string} - e.g. "FmTealAlphaFeedDefsArtist"
38
+ */
39
+ export function refToTypeName(refUri: string): string;
40
+ /**
41
+ * Parse a lexicon JSON object into structured form
42
+ * @param {RawLexiconJson} json - Raw lexicon JSON
43
+ * @returns {Lexicon}
44
+ */
45
+ export function parseLexicon(json: RawLexiconJson): Lexicon;
46
+ /**
47
+ * Build a GraphQL schema from parsed lexicons
48
+ * @param {Lexicon[]} lexicons
49
+ * @returns {GraphQLSchema}
50
+ */
51
+ export function buildSchema(lexicons: Lexicon[]): GraphQLSchema;
52
+ /**
53
+ * Create a GraphQL adapter with query resolvers
54
+ * @param {Lexicon[]} lexicons
55
+ * @param {AdapterOptions} options
56
+ * @returns {{
57
+ * schema: GraphQLSchema,
58
+ * execute: (query: string, variables?: Record<string, unknown>) => Promise<any>,
59
+ * subscribe: (query: string, variables?: Record<string, unknown>) => Promise<AsyncIterable<import('graphql').ExecutionResult>>
60
+ * }}
61
+ */
62
+ export function createAdapter(
63
+ lexicons: Lexicon[],
64
+ options: AdapterOptions,
65
+ ): {
66
+ schema: GraphQLSchema;
67
+ execute: (query: string, variables?: Record<string, unknown>) => Promise<any>;
68
+ subscribe: (
69
+ query: string,
70
+ variables?: Record<string, unknown>,
71
+ ) => Promise<AsyncIterable<import('graphql').ExecutionResult>>;
72
+ };
73
+ export namespace ErrorCodes {
74
+ let INVALID_LEXICON: string;
75
+ let UNSUPPORTED_VERSION: string;
76
+ let QUERY_FAILED: string;
77
+ let VALIDATION_FAILED: string;
78
+ }
79
+ /**
80
+ * Custom error class for lex-gql errors
81
+ */
82
+ export class LexGqlError extends Error {
83
+ /**
84
+ * @param {string} message
85
+ * @param {string} code
86
+ * @param {Object} [details]
87
+ */
88
+ constructor(message: string, code: string, details?: Object);
89
+ code: string;
90
+ details: Object;
91
+ }
92
+ export type WhereClause = {
93
+ field: string;
94
+ op: string;
95
+ value: any;
96
+ };
97
+ export type SortClause = {
98
+ field: string;
99
+ dir: string;
100
+ };
101
+ export type Pagination = {
102
+ first?: number | undefined;
103
+ after?: string | undefined;
104
+ last?: number | undefined;
105
+ before?: string | undefined;
106
+ };
107
+ export type Aggregate = {
108
+ field: string;
109
+ fn: string;
110
+ };
111
+ export type Operation = {
112
+ type: 'findMany' | 'findOne' | 'count' | 'aggregate' | 'create' | 'update' | 'delete';
113
+ collection: string;
114
+ where?: WhereClause[] | undefined;
115
+ select?: string[] | undefined;
116
+ sort?: SortClause[] | undefined;
117
+ pagination?: Pagination | undefined;
118
+ data?: Record<string, any> | undefined;
119
+ uri?: string | undefined;
120
+ rkey?: string | undefined;
121
+ groupBy?: string[] | undefined;
122
+ aggregates?: Aggregate[] | undefined;
123
+ };
124
+ export type AdapterOptions = {
125
+ query: (op: Operation) => Promise<any>;
126
+ subscribe?: ((op: SubscribeOperation) => AsyncIterable<any>) | undefined;
127
+ context?: Record<string, any> | undefined;
128
+ maxDepth?: number | undefined;
129
+ };
130
+ export type SubscriptionEvent = 'created' | 'updated' | 'deleted';
131
+ export type SubscribeOperation = {
132
+ /**
133
+ * - The collection NSID (e.g., 'app.bsky.feed.post')
134
+ */
135
+ collection: string;
136
+ /**
137
+ * - The event type
138
+ */
139
+ event: SubscriptionEvent;
140
+ };
141
+ export type Property = {
142
+ name: string;
143
+ type: string;
144
+ required: boolean;
145
+ format?: string | null | undefined;
146
+ ref?: string | null | undefined;
147
+ refs?: string[] | null | undefined;
148
+ items?: ArrayItems | null | undefined;
149
+ };
150
+ export type ArrayItems = {
151
+ type: string;
152
+ ref?: string | null | undefined;
153
+ refs?: string[] | null | undefined;
154
+ };
155
+ export type RecordDef = {
156
+ type: string;
157
+ key?: string | null | undefined;
158
+ properties: Property[];
159
+ };
160
+ export type Lexicon = {
161
+ id: string;
162
+ defs: {
163
+ main: RecordDef | null;
164
+ others: Record<string, RecordDef>;
165
+ };
166
+ };
167
+ export type RawLexiconJson = {
168
+ id: string;
169
+ lexicon?: number | undefined;
170
+ defs?: Record<string, any> | undefined;
171
+ };
172
+ import { GraphQLSchema } from 'graphql';