lex-gql 0.1.0 → 0.2.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 +94 -0
- package/package.json +7 -6
- package/src/lex-gql.d.ts +271 -0
- package/{lex-gql.js → src/lex-gql.js} +632 -90
- package/lex-gql.d.ts +0 -172
package/README.md
CHANGED
|
@@ -133,6 +133,100 @@ import {
|
|
|
133
133
|
} from 'lex-gql'
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## Query Port Interface
|
|
137
|
+
|
|
138
|
+
lex-gql follows the hexagonal architecture pattern. Your data layer implements the **query port** interface:
|
|
139
|
+
|
|
140
|
+
### Operation Types
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
type Operation =
|
|
144
|
+
| { type: 'findMany'; collection: string; where: WhereClause[]; pagination: Pagination; sort?: SortClause[] }
|
|
145
|
+
| { type: 'aggregate'; collection: string; where: WhereClause[]; groupBy?: string[] }
|
|
146
|
+
| { type: 'create'; collection: string; rkey?: string; record: object }
|
|
147
|
+
| { type: 'update'; collection: string; rkey: string; record: object }
|
|
148
|
+
| { type: 'delete'; collection: string; rkey: string }
|
|
149
|
+
|
|
150
|
+
// Field condition
|
|
151
|
+
type FieldCondition = { field: string; op: 'eq' | 'in' | 'contains' | 'gt' | 'gte' | 'lt' | 'lte'; value: any }
|
|
152
|
+
|
|
153
|
+
// Logical operators (for AND/OR queries)
|
|
154
|
+
type LogicalCondition = { op: 'and' | 'or'; conditions: WhereClause[][] }
|
|
155
|
+
|
|
156
|
+
type WhereClause = FieldCondition | LogicalCondition
|
|
157
|
+
type SortClause = { field: string; dir: 'asc' | 'desc' }
|
|
158
|
+
type Pagination = { first?: number; after?: string; last?: number; before?: string }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Cross-Collection URI Resolution
|
|
162
|
+
|
|
163
|
+
For batched forward join resolution, lex-gql issues `findMany` operations with `collection: '*'`. This special value means "query across all collections by URI":
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Forward join batch request
|
|
167
|
+
{
|
|
168
|
+
type: 'findMany',
|
|
169
|
+
collection: '*', // Special: resolve by URI, ignore collection filter
|
|
170
|
+
where: [{ field: 'uri', op: 'in', value: ['at://did1/...', 'at://did2/...'] }],
|
|
171
|
+
pagination: {}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Adapters **must** handle this case by omitting the collection filter and returning records matching the URIs. The returned records must include a `collection` field for union type resolution.
|
|
176
|
+
|
|
177
|
+
### Response Format
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// For findMany
|
|
181
|
+
{ rows: Record[]; hasNext: boolean; hasPrev: boolean }
|
|
182
|
+
|
|
183
|
+
// For aggregate
|
|
184
|
+
{ count: number; groups: { [field]: value; count: number }[] }
|
|
185
|
+
|
|
186
|
+
// For mutations
|
|
187
|
+
Record | { uri: string }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Standard Records Schema
|
|
191
|
+
|
|
192
|
+
For SQL-based adapters, we recommend this schema:
|
|
193
|
+
|
|
194
|
+
```sql
|
|
195
|
+
CREATE TABLE records (
|
|
196
|
+
uri TEXT PRIMARY KEY,
|
|
197
|
+
did TEXT NOT NULL,
|
|
198
|
+
collection TEXT NOT NULL,
|
|
199
|
+
rkey TEXT NOT NULL,
|
|
200
|
+
cid TEXT,
|
|
201
|
+
record TEXT NOT NULL, -- JSON blob
|
|
202
|
+
indexed_at TEXT NOT NULL
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
CREATE INDEX idx_records_collection ON records(collection);
|
|
206
|
+
CREATE INDEX idx_records_did ON records(did);
|
|
207
|
+
|
|
208
|
+
CREATE TABLE actors (
|
|
209
|
+
did TEXT PRIMARY KEY,
|
|
210
|
+
handle TEXT NOT NULL
|
|
211
|
+
);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Hydration Helpers
|
|
215
|
+
|
|
216
|
+
Use these helpers to transform database rows into lex-gql format:
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
import { hydrateBlobs, hydrateRecord } from 'lex-gql';
|
|
220
|
+
|
|
221
|
+
// hydrateBlobs - inject DID into blob fields for URL resolution
|
|
222
|
+
const record = JSON.parse(row.record);
|
|
223
|
+
const hydrated = hydrateBlobs(record, row.did);
|
|
224
|
+
|
|
225
|
+
// hydrateRecord - full transformation from standard schema
|
|
226
|
+
const rows = db.query('SELECT r.*, a.handle FROM records r LEFT JOIN actors a ON r.did = a.did');
|
|
227
|
+
const records = rows.map(hydrateRecord);
|
|
228
|
+
```
|
|
229
|
+
|
|
136
230
|
## Generated Schema Structure
|
|
137
231
|
|
|
138
232
|
For each record lexicon, lex-gql generates:
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lex-gql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Generate a complete GraphQL API from AT Protocol lexicons",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "lex-gql.js",
|
|
8
|
-
"types": "lex-gql.d.ts",
|
|
7
|
+
"main": "src/lex-gql.js",
|
|
8
|
+
"types": "src/lex-gql.d.ts",
|
|
9
9
|
"files": [
|
|
10
|
-
"lex-gql.js",
|
|
11
|
-
"lex-gql.d.ts"
|
|
10
|
+
"src/lex-gql.js",
|
|
11
|
+
"src/lex-gql.d.ts"
|
|
12
12
|
],
|
|
13
13
|
"keywords": [
|
|
14
14
|
"graphql",
|
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
"vitest": "^1.0.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
38
39
|
"test": "vitest run",
|
|
39
40
|
"test:watch": "vitest",
|
|
40
|
-
"typecheck": "tsc"
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
41
42
|
}
|
|
42
43
|
}
|
package/src/lex-gql.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
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
|
+
* Resolve a ref string to a full registry key
|
|
36
|
+
* @param {string} ref - The ref string (e.g., "#replyRef" or "app.bsky.embed.images")
|
|
37
|
+
* @param {string} parentLexiconId - The lexicon ID containing this ref
|
|
38
|
+
* @returns {string} Full registry key
|
|
39
|
+
*/
|
|
40
|
+
export function resolveRefKey(ref: string, parentLexiconId: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Convert a ref URI to a GraphQL type name
|
|
43
|
+
* @param {string} refUri - e.g. "fm.teal.alpha.feed.defs#artist"
|
|
44
|
+
* @returns {string} - e.g. "FmTealAlphaFeedDefsArtist"
|
|
45
|
+
*/
|
|
46
|
+
export function refToTypeName(refUri: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Inject DID into blob objects for URL resolution.
|
|
49
|
+
* Blobs need the parent record's DID to generate CDN URLs.
|
|
50
|
+
*
|
|
51
|
+
* @param {any} obj - Record object or value to hydrate
|
|
52
|
+
* @param {string} did - DID to inject into blob objects
|
|
53
|
+
* @returns {any} - Hydrated object with did added to blobs
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const record = JSON.parse(row.record);
|
|
57
|
+
* const hydrated = hydrateBlobs(record, row.did);
|
|
58
|
+
*/
|
|
59
|
+
export function hydrateBlobs(obj: any, did: string): any;
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} DatabaseRow
|
|
62
|
+
* @property {string} uri - Record AT URI
|
|
63
|
+
* @property {string} did - Author DID
|
|
64
|
+
* @property {string} collection - Lexicon NSID
|
|
65
|
+
* @property {string} [cid] - Content ID
|
|
66
|
+
* @property {string|Object} record - JSON string or parsed object
|
|
67
|
+
* @property {string} indexed_at - ISO timestamp
|
|
68
|
+
* @property {string} [handle] - Actor handle from actors table join
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Transform a database row into lex-gql record format.
|
|
72
|
+
* Expects the standard records table schema.
|
|
73
|
+
*
|
|
74
|
+
* Standard schema:
|
|
75
|
+
* - uri: TEXT (record AT URI)
|
|
76
|
+
* - did: TEXT (author DID)
|
|
77
|
+
* - collection: TEXT (lexicon NSID)
|
|
78
|
+
* - cid: TEXT (optional, content ID)
|
|
79
|
+
* - record: TEXT (JSON) or Object
|
|
80
|
+
* - indexed_at: TEXT (ISO timestamp)
|
|
81
|
+
* - handle: TEXT (optional, actor handle from actors table join)
|
|
82
|
+
*
|
|
83
|
+
* @param {DatabaseRow} row - Database row
|
|
84
|
+
* @returns {Record<string, any>} - Hydrated record for lex-gql
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const rows = db.query('SELECT r.*, a.handle FROM records r LEFT JOIN actors a ON r.did = a.did');
|
|
88
|
+
* const records = rows.map(hydrateRecord);
|
|
89
|
+
*/
|
|
90
|
+
export function hydrateRecord(row: DatabaseRow): Record<string, any>;
|
|
91
|
+
/**
|
|
92
|
+
* Parse a lexicon JSON object into structured form
|
|
93
|
+
* @param {RawLexiconJson} json - Raw lexicon JSON
|
|
94
|
+
* @returns {Lexicon}
|
|
95
|
+
*/
|
|
96
|
+
export function parseLexicon(json: RawLexiconJson): Lexicon;
|
|
97
|
+
/**
|
|
98
|
+
* Build a GraphQL schema from parsed lexicons
|
|
99
|
+
* @param {Lexicon[]} lexicons
|
|
100
|
+
* @returns {GraphQLSchema}
|
|
101
|
+
*/
|
|
102
|
+
export function buildSchema(lexicons: Lexicon[]): GraphQLSchema;
|
|
103
|
+
/**
|
|
104
|
+
* Create a GraphQL adapter with query resolvers
|
|
105
|
+
* @param {Lexicon[]} lexicons
|
|
106
|
+
* @param {AdapterOptions} options
|
|
107
|
+
* @returns {{
|
|
108
|
+
* schema: GraphQLSchema,
|
|
109
|
+
* execute: (query: string, variables?: Record<string, unknown>) => Promise<any>,
|
|
110
|
+
* subscribe: (query: string, variables?: Record<string, unknown>) => Promise<AsyncIterable<import('graphql').ExecutionResult>>
|
|
111
|
+
* }}
|
|
112
|
+
*/
|
|
113
|
+
export function createAdapter(lexicons: Lexicon[], options: AdapterOptions): {
|
|
114
|
+
schema: GraphQLSchema;
|
|
115
|
+
execute: (query: string, variables?: Record<string, unknown>) => Promise<any>;
|
|
116
|
+
subscribe: (query: string, variables?: Record<string, unknown>) => Promise<AsyncIterable<import("graphql").ExecutionResult>>;
|
|
117
|
+
};
|
|
118
|
+
export namespace ErrorCodes {
|
|
119
|
+
let INVALID_LEXICON: string;
|
|
120
|
+
let UNSUPPORTED_VERSION: string;
|
|
121
|
+
let QUERY_FAILED: string;
|
|
122
|
+
let VALIDATION_FAILED: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Custom error class for lex-gql errors
|
|
126
|
+
*/
|
|
127
|
+
export class LexGqlError extends Error {
|
|
128
|
+
/**
|
|
129
|
+
* @param {string} message
|
|
130
|
+
* @param {string} code
|
|
131
|
+
* @param {Object} [details]
|
|
132
|
+
*/
|
|
133
|
+
constructor(message: string, code: string, details?: Object);
|
|
134
|
+
code: string;
|
|
135
|
+
details: Object;
|
|
136
|
+
}
|
|
137
|
+
export type DatabaseRow = {
|
|
138
|
+
/**
|
|
139
|
+
* - Record AT URI
|
|
140
|
+
*/
|
|
141
|
+
uri: string;
|
|
142
|
+
/**
|
|
143
|
+
* - Author DID
|
|
144
|
+
*/
|
|
145
|
+
did: string;
|
|
146
|
+
/**
|
|
147
|
+
* - Lexicon NSID
|
|
148
|
+
*/
|
|
149
|
+
collection: string;
|
|
150
|
+
/**
|
|
151
|
+
* - Content ID
|
|
152
|
+
*/
|
|
153
|
+
cid?: string | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* - JSON string or parsed object
|
|
156
|
+
*/
|
|
157
|
+
record: string | Object;
|
|
158
|
+
/**
|
|
159
|
+
* - ISO timestamp
|
|
160
|
+
*/
|
|
161
|
+
indexed_at: string;
|
|
162
|
+
/**
|
|
163
|
+
* - Actor handle from actors table join
|
|
164
|
+
*/
|
|
165
|
+
handle?: string | undefined;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* A where clause for filtering records.
|
|
169
|
+
* Can be a field condition or a logical operator (AND/OR).
|
|
170
|
+
*
|
|
171
|
+
* Field condition: { field: 'text', op: 'eq', value: 'hello' }
|
|
172
|
+
* Logical AND: { op: 'and', conditions: WhereClause[][] }
|
|
173
|
+
* Logical OR: { op: 'or', conditions: WhereClause[][] }
|
|
174
|
+
*/
|
|
175
|
+
export type WhereClause = {
|
|
176
|
+
/**
|
|
177
|
+
* - Field name (for field conditions)
|
|
178
|
+
*/
|
|
179
|
+
field?: string | undefined;
|
|
180
|
+
/**
|
|
181
|
+
* - Operator: 'eq' | 'in' | 'contains' | 'gt' | 'gte' | 'lt' | 'lte' | 'and' | 'or'
|
|
182
|
+
*/
|
|
183
|
+
op: string;
|
|
184
|
+
/**
|
|
185
|
+
* - Value to compare (for field conditions)
|
|
186
|
+
*/
|
|
187
|
+
value?: any;
|
|
188
|
+
/**
|
|
189
|
+
* - Nested conditions (for and/or)
|
|
190
|
+
*/
|
|
191
|
+
conditions?: WhereClause[][] | undefined;
|
|
192
|
+
};
|
|
193
|
+
export type SortClause = {
|
|
194
|
+
field: string;
|
|
195
|
+
dir: string;
|
|
196
|
+
};
|
|
197
|
+
export type Pagination = {
|
|
198
|
+
first?: number | undefined;
|
|
199
|
+
after?: string | undefined;
|
|
200
|
+
last?: number | undefined;
|
|
201
|
+
before?: string | undefined;
|
|
202
|
+
};
|
|
203
|
+
export type Aggregate = {
|
|
204
|
+
field: string;
|
|
205
|
+
fn: string;
|
|
206
|
+
};
|
|
207
|
+
export type Operation = {
|
|
208
|
+
type: "findMany" | "findOne" | "count" | "aggregate" | "create" | "update" | "delete";
|
|
209
|
+
collection: string;
|
|
210
|
+
where?: WhereClause[] | undefined;
|
|
211
|
+
select?: string[] | undefined;
|
|
212
|
+
sort?: SortClause[] | undefined;
|
|
213
|
+
pagination?: Pagination | undefined;
|
|
214
|
+
data?: Record<string, any> | undefined;
|
|
215
|
+
uri?: string | undefined;
|
|
216
|
+
rkey?: string | undefined;
|
|
217
|
+
groupBy?: string[] | undefined;
|
|
218
|
+
aggregates?: Aggregate[] | undefined;
|
|
219
|
+
limit?: number | undefined;
|
|
220
|
+
orderBy?: "COUNT_ASC" | "COUNT_DESC" | undefined;
|
|
221
|
+
arrayFields?: string[] | undefined;
|
|
222
|
+
};
|
|
223
|
+
export type AdapterOptions = {
|
|
224
|
+
query: (op: Operation) => Promise<any>;
|
|
225
|
+
subscribe?: ((op: SubscribeOperation) => AsyncIterable<any>) | undefined;
|
|
226
|
+
context?: Record<string, any> | undefined;
|
|
227
|
+
maxDepth?: number | undefined;
|
|
228
|
+
};
|
|
229
|
+
export type SubscriptionEvent = "created" | "updated" | "deleted";
|
|
230
|
+
export type SubscribeOperation = {
|
|
231
|
+
/**
|
|
232
|
+
* - The collection NSID (e.g., 'app.bsky.feed.post')
|
|
233
|
+
*/
|
|
234
|
+
collection: string;
|
|
235
|
+
/**
|
|
236
|
+
* - The event type
|
|
237
|
+
*/
|
|
238
|
+
event: SubscriptionEvent;
|
|
239
|
+
};
|
|
240
|
+
export type Property = {
|
|
241
|
+
name: string;
|
|
242
|
+
type: string;
|
|
243
|
+
required: boolean;
|
|
244
|
+
format?: string | null | undefined;
|
|
245
|
+
ref?: string | null | undefined;
|
|
246
|
+
refs?: string[] | null | undefined;
|
|
247
|
+
items?: ArrayItems | null | undefined;
|
|
248
|
+
};
|
|
249
|
+
export type ArrayItems = {
|
|
250
|
+
type: string;
|
|
251
|
+
ref?: string | null | undefined;
|
|
252
|
+
refs?: string[] | null | undefined;
|
|
253
|
+
};
|
|
254
|
+
export type RecordDef = {
|
|
255
|
+
type: string;
|
|
256
|
+
key?: string | null | undefined;
|
|
257
|
+
properties: Property[];
|
|
258
|
+
};
|
|
259
|
+
export type Lexicon = {
|
|
260
|
+
id: string;
|
|
261
|
+
defs: {
|
|
262
|
+
main: RecordDef | null;
|
|
263
|
+
others: Record<string, RecordDef>;
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
export type RawLexiconJson = {
|
|
267
|
+
id: string;
|
|
268
|
+
lexicon?: number | undefined;
|
|
269
|
+
defs?: Record<string, any> | undefined;
|
|
270
|
+
};
|
|
271
|
+
import { GraphQLSchema } from 'graphql';
|