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.
- package/README.md +232 -0
- package/lex-gql.d.ts +172 -0
- package/lex-gql.js +1915 -0
- 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';
|