@aexol/axolotl 2.1.0 → 2.1.1
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 +846 -19
- package/lib/index.js +11 -14
- package/lib/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,29 +1,856 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Axolotl Framework - LLM Integration Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Axolotl is a **type-safe, schema-first GraphQL framework** that generates TypeScript types from your GraphQL schema and provides full type safety for resolvers. This guide provides exact instructions for LLMs to work with Axolotl projects.
|
|
6
|
+
|
|
7
|
+
## Core Concepts
|
|
8
|
+
|
|
9
|
+
### 1. Schema-First Development
|
|
10
|
+
|
|
11
|
+
- Write GraphQL schema in `.graphql` files
|
|
12
|
+
- Axolotl CLI generates TypeScript types automatically
|
|
13
|
+
- Resolvers are fully typed based on the schema
|
|
14
|
+
|
|
15
|
+
### 2. File Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
project/
|
|
19
|
+
├── axolotl.json # Configuration file
|
|
20
|
+
├── schema.graphql # GraphQL schema
|
|
21
|
+
├── src/
|
|
22
|
+
│ ├── axolotl.ts # Framework initialization
|
|
23
|
+
│ ├── models.ts # Auto-generated types (DO NOT EDIT)
|
|
24
|
+
│ ├── resolvers.ts # Resolver implementations
|
|
25
|
+
│ └── index.ts # Server entry point
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Critical Rules for LLMs
|
|
31
|
+
|
|
32
|
+
**ALWAYS follow these rules when working with Axolotl:**
|
|
33
|
+
|
|
34
|
+
1. **NEVER edit models.ts manually** - always regenerate with `axolotl build`
|
|
35
|
+
2. **ALWAYS use .js extensions** in imports (ESM requirement)
|
|
36
|
+
3. **ALWAYS run axolotl build** after schema changes
|
|
37
|
+
4. **CRITICAL: Resolver signature is** `(input, args)` where `input = [source, args, context]`
|
|
38
|
+
5. **CRITICAL: Access context as** `input[2]` or `([, , context])`
|
|
39
|
+
6. **CRITICAL: Access parent/source as** `input[0]` or `([source])`
|
|
40
|
+
7. **CRITICAL: Context type must** extend `YogaInitialContext` and spread `...initial`
|
|
41
|
+
8. **Import from axolotl.ts** - never from @aexol/axolotl-core directly in resolver files
|
|
42
|
+
9. **Use createResolvers()** for ALL resolver definitions
|
|
43
|
+
10. **Use mergeAxolotls()** to combine multiple resolver sets
|
|
44
|
+
11. **Return empty object `{}`** for nested resolver enablement
|
|
45
|
+
12. **Context typing** requires `graphqlYogaWithContextAdapter<T>(contextFunction)`
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## STEP 1: Understanding axolotl.json
|
|
50
|
+
|
|
51
|
+
The `axolotl.json` configuration file defines:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"schema": "schema.graphql", // Path to main schema
|
|
56
|
+
"models": "src/models.ts", // Where to generate types
|
|
57
|
+
"federation": [
|
|
58
|
+
// Optional: for micro-federation
|
|
59
|
+
{
|
|
60
|
+
"schema": "src/todos/schema.graphql",
|
|
61
|
+
"models": "src/todos/models.ts"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"zeus": [
|
|
65
|
+
// Optional: GraphQL client generation
|
|
66
|
+
{
|
|
67
|
+
"generationPath": "src/"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Instructions:**
|
|
74
|
+
|
|
75
|
+
- Read `axolotl.json` first to understand project structure
|
|
76
|
+
- NEVER edit `axolotl.json` unless explicitly asked
|
|
77
|
+
- Use paths from config to locate schema and models
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## STEP 2: GraphQL Schema (schema.graphql)
|
|
82
|
+
|
|
83
|
+
**Example:**
|
|
84
|
+
|
|
85
|
+
```graphql
|
|
86
|
+
scalar Secret
|
|
87
|
+
|
|
88
|
+
type User {
|
|
89
|
+
_id: String!
|
|
90
|
+
username: String!
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Query {
|
|
94
|
+
user: AuthorizedUserQuery @resolver
|
|
95
|
+
hello: String!
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type Mutation {
|
|
99
|
+
login(username: String!, password: String!): String! @resolver
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
directive @resolver on FIELD_DEFINITION
|
|
103
|
+
|
|
104
|
+
schema {
|
|
105
|
+
query: Query
|
|
106
|
+
mutation: Mutation
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Key Points:**
|
|
111
|
+
|
|
112
|
+
- This is the source of truth for your API
|
|
113
|
+
- The `@resolver` directive marks fields that need resolver implementations
|
|
114
|
+
- After modifying schema, ALWAYS run: `npx @aexol/axolotl build`
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## STEP 3: Models Generation
|
|
119
|
+
|
|
120
|
+
**Command:**
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx @aexol/axolotl build
|
|
124
|
+
# Or with custom directory:
|
|
125
|
+
npx @aexol/axolotl build --cwd path/to/project
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**What it does:**
|
|
129
|
+
|
|
130
|
+
- Reads `schema.graphql`
|
|
131
|
+
- Generates TypeScript types in `src/models.ts`
|
|
132
|
+
- Creates type definitions for Query, Mutation, Subscription, and all types
|
|
133
|
+
|
|
134
|
+
**Generated models.ts structure:**
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// AUTO-GENERATED - DO NOT EDIT
|
|
138
|
+
|
|
139
|
+
export type Scalars = {
|
|
140
|
+
['Secret']: unknown;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export type Models<S extends { [P in keyof Scalars]: any }> = {
|
|
144
|
+
['User']: {
|
|
145
|
+
_id: { args: Record<string, never> };
|
|
146
|
+
username: { args: Record<string, never> };
|
|
147
|
+
};
|
|
148
|
+
['Query']: {
|
|
149
|
+
hello: { args: Record<string, never> };
|
|
150
|
+
user: { args: Record<string, never> };
|
|
151
|
+
};
|
|
152
|
+
['Mutation']: {
|
|
153
|
+
login: {
|
|
154
|
+
args: {
|
|
155
|
+
username: string;
|
|
156
|
+
password: string;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## STEP 3.5: Generate Resolver Boilerplate (Optional but Recommended)
|
|
166
|
+
|
|
167
|
+
**Command:**
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx @aexol/axolotl resolvers
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**What it does:**
|
|
174
|
+
|
|
175
|
+
- Reads your schema and finds all fields marked with `@resolver` directive
|
|
176
|
+
- Generates organized resolver file structure automatically
|
|
177
|
+
- Creates placeholder implementations for each resolver field
|
|
178
|
+
- Sets up proper import structure and type safety
|
|
179
|
+
|
|
180
|
+
**Generated structure example:**
|
|
181
|
+
|
|
182
|
+
Given a schema with `@resolver` directives:
|
|
183
|
+
|
|
184
|
+
```graphql
|
|
185
|
+
type Query {
|
|
186
|
+
user: AuthorizedUserQuery @resolver
|
|
187
|
+
hello: String!
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type Mutation {
|
|
191
|
+
login(username: String!, password: String!): String! @resolver
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The command generates:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
src/
|
|
199
|
+
├── resolvers/
|
|
200
|
+
│ ├── Query/
|
|
201
|
+
│ │ ├── user.ts # Individual field resolver
|
|
202
|
+
│ │ └── resolvers.ts # Query type aggregator
|
|
203
|
+
│ ├── Mutation/
|
|
204
|
+
│ │ ├── login.ts # Individual field resolver
|
|
205
|
+
│ │ └── resolvers.ts # Mutation type aggregator
|
|
206
|
+
│ └── resolvers.ts # Root aggregator (export this)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Generated file example (Query/user.ts):**
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { createResolvers } from '../../axolotl.js';
|
|
213
|
+
|
|
214
|
+
export default createResolvers({
|
|
215
|
+
Query: {
|
|
216
|
+
user: async ([parent, details, ctx], args) => {
|
|
217
|
+
// TODO: implement resolver for Query.user
|
|
218
|
+
throw new Error('Not implemented: Query.user');
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Generated aggregator (Query/resolvers.ts):**
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { createResolvers } from '../../axolotl.js';
|
|
228
|
+
import user from './user.js';
|
|
229
|
+
|
|
230
|
+
export default createResolvers({
|
|
231
|
+
Query: {
|
|
232
|
+
...user.Query,
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Root aggregator (resolvers/resolvers.ts):**
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { createResolvers } from '../axolotl.js';
|
|
241
|
+
import Query from './Query/resolvers.js';
|
|
242
|
+
import Mutation from './Mutation/resolvers.js';
|
|
243
|
+
|
|
244
|
+
export default createResolvers({
|
|
245
|
+
...Query,
|
|
246
|
+
...Mutation,
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Key Benefits:**
|
|
251
|
+
|
|
252
|
+
- **Automatic scaffolding** - No manual file/folder creation needed
|
|
253
|
+
- **Organized structure** - Each resolver in its own file
|
|
254
|
+
- **Type safety** - All generated files use `createResolvers()` correctly
|
|
255
|
+
- **Non-destructive** - Only creates files that don't exist (won't overwrite your implementations)
|
|
256
|
+
- **Aggregator files always updated** - Type-level and root aggregators are regenerated to stay in sync
|
|
257
|
+
|
|
258
|
+
**When to use:**
|
|
259
|
+
|
|
260
|
+
- ✅ Starting a new project with many resolvers
|
|
261
|
+
- ✅ Adding new resolver fields to existing schema
|
|
262
|
+
- ✅ Want organized, maintainable resolver structure
|
|
263
|
+
- ✅ Working with federated schemas (generates for each module)
|
|
264
|
+
|
|
265
|
+
**Workflow:**
|
|
266
|
+
|
|
267
|
+
1. Add `@resolver` directives to schema fields
|
|
268
|
+
2. Run `npx @aexol/axolotl build` to update types
|
|
269
|
+
3. Run `npx @aexol/axolotl resolvers` to scaffold structure
|
|
270
|
+
4. Implement TODO sections in generated resolver files
|
|
271
|
+
5. Import and use `resolvers/resolvers.ts` in your server
|
|
272
|
+
|
|
273
|
+
**Note for Federated Projects:**
|
|
274
|
+
|
|
275
|
+
The command automatically detects federation in `axolotl.json` and generates resolver structures for each federated schema in the appropriate directories.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## STEP 4: Creating axolotl.ts
|
|
280
|
+
|
|
281
|
+
**Purpose:** Initialize Axolotl framework with adapter and type definitions.
|
|
282
|
+
|
|
283
|
+
**File: src/axolotl.ts**
|
|
284
|
+
|
|
285
|
+
### Without Custom Context (Basic)
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { Models, Scalars } from '@/src/models.js';
|
|
289
|
+
import { Axolotl } from '@aexol/axolotl-core';
|
|
290
|
+
import { graphqlYogaAdapter } from '@aexol/axolotl-graphql-yoga';
|
|
291
|
+
|
|
292
|
+
export const { applyMiddleware, createResolvers, createDirectives, adapter } = Axolotl(graphqlYogaAdapter)<
|
|
293
|
+
Models<{ Secret: number }>, // Models with scalar mappings
|
|
294
|
+
Scalars // Scalar type definitions
|
|
295
|
+
>();
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### With Custom Context (Recommended)
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { Models, Scalars } from '@/src/models.js';
|
|
302
|
+
import { Axolotl } from '@aexol/axolotl-core';
|
|
303
|
+
import { graphqlYogaWithContextAdapter } from '@aexol/axolotl-graphql-yoga';
|
|
304
|
+
import { YogaInitialContext } from 'graphql-yoga';
|
|
305
|
+
|
|
306
|
+
// Define your context type - MUST extend YogaInitialContext
|
|
307
|
+
type AppContext = YogaInitialContext & {
|
|
308
|
+
userId: string | null;
|
|
309
|
+
isAuthenticated: boolean;
|
|
310
|
+
isAdmin: boolean;
|
|
311
|
+
requestId: string;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Context builder function
|
|
315
|
+
async function buildContext(initial: YogaInitialContext): Promise<AppContext> {
|
|
316
|
+
const token = initial.request.headers.get('authorization')?.replace('Bearer ', '');
|
|
317
|
+
const user = token ? await verifyToken(token) : null;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
...initial, // ✅ MUST spread initial context
|
|
321
|
+
userId: user?._id || null,
|
|
322
|
+
isAuthenticated: !!user,
|
|
323
|
+
isAdmin: user?.role === 'admin' || false,
|
|
324
|
+
requestId: crypto.randomUUID(),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const { createResolvers, adapter } = Axolotl(graphqlYogaWithContextAdapter<AppContext>(buildContext))<
|
|
329
|
+
Models<{ Secret: number }>,
|
|
330
|
+
Scalars
|
|
331
|
+
>();
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Key Components:**
|
|
335
|
+
|
|
336
|
+
1. **Import Models & Scalars** from generated `models.ts`
|
|
337
|
+
2. **Import Axolotl** from `@aexol/axolotl-core`
|
|
338
|
+
3. **Import adapter** (GraphQL Yoga in this case)
|
|
339
|
+
4. **Initialize with generics:**
|
|
340
|
+
- First generic: `Models<ScalarMap>` - your type definitions
|
|
341
|
+
- Second generic: `Scalars` - custom scalar types
|
|
342
|
+
|
|
343
|
+
**Exported functions:**
|
|
344
|
+
|
|
345
|
+
- `createResolvers()` - Create type-safe resolvers
|
|
346
|
+
- `createDirectives()` - Create custom directives
|
|
347
|
+
- `applyMiddleware()` - Apply middleware to resolvers
|
|
348
|
+
- `adapter()` - Configure and start server
|
|
349
|
+
|
|
350
|
+
**Context Type Safety:**
|
|
351
|
+
|
|
352
|
+
- `graphqlYogaWithContextAdapter<T>()` takes a **FUNCTION** (not an object)
|
|
353
|
+
- Your context type MUST extend `YogaInitialContext`
|
|
354
|
+
- The function MUST return an object that includes `...initial`
|
|
355
|
+
- Context is automatically typed in ALL resolvers
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## STEP 5: Writing Resolvers
|
|
360
|
+
|
|
361
|
+
### Resolver Signature
|
|
362
|
+
|
|
363
|
+
**The resolver signature is:**
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
(input, args) => ReturnType;
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Where:
|
|
370
|
+
|
|
371
|
+
- **`input`** is a tuple: `[source, args, context]`
|
|
372
|
+
- `input[0]` = **source** (parent value)
|
|
373
|
+
- `input[1]` = **args** (field arguments)
|
|
374
|
+
- `input[2]` = **context** (request context)
|
|
375
|
+
- **`args`** is also provided as second parameter for convenience
|
|
376
|
+
|
|
377
|
+
### Simple Resolver Example
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import { createResolvers } from '@/src/axolotl.js';
|
|
381
|
+
|
|
382
|
+
export default createResolvers({
|
|
383
|
+
Query: {
|
|
384
|
+
hello: async ([source, args, context]) => {
|
|
385
|
+
// ↑ ↑ ↑
|
|
386
|
+
// input[0] [1] [2]
|
|
387
|
+
return 'Hello, World!';
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
Mutation: {
|
|
391
|
+
login: async ([source, args, context], { username, password }) => {
|
|
392
|
+
// ↑ Destructure tuple ↑ Convenience args parameter
|
|
393
|
+
const token = await authenticateUser(username, password);
|
|
394
|
+
return token;
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Common Destructuring Patterns
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// Pattern 1: Access context only
|
|
404
|
+
createResolvers({
|
|
405
|
+
Query: {
|
|
406
|
+
me: async ([, , context]) => {
|
|
407
|
+
return getUserById(context.userId);
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Pattern 2: Access source and context
|
|
413
|
+
createResolvers({
|
|
414
|
+
AuthorizedUserQuery: {
|
|
415
|
+
todos: async ([source, , context]) => {
|
|
416
|
+
const src = source as { _id: string };
|
|
417
|
+
return getTodosByUserId(src._id);
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Pattern 3: Use convenience args parameter
|
|
423
|
+
createResolvers({
|
|
424
|
+
Mutation: {
|
|
425
|
+
createTodo: async ([, , context], { content }) => {
|
|
426
|
+
return createTodo(content, context.userId);
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Pattern 4: Ignore unused with underscores
|
|
432
|
+
createResolvers({
|
|
433
|
+
Query: {
|
|
434
|
+
me: async ([_, __, context]) => {
|
|
435
|
+
return getUserById(context.userId);
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Accessing Parent (Source) in Nested Resolvers
|
|
442
|
+
|
|
443
|
+
In nested resolvers, the **parent** (also called **source**) is the value returned by the parent resolver.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// Schema
|
|
447
|
+
type Query {
|
|
448
|
+
user: AuthorizedUserQuery @resolver
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
type AuthorizedUserQuery {
|
|
452
|
+
me: User! @resolver
|
|
453
|
+
todos: [Todo!] @resolver
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Resolvers
|
|
457
|
+
createResolvers({
|
|
458
|
+
Query: {
|
|
459
|
+
user: async ([, , context]) => {
|
|
460
|
+
const token = context.request.headers.get('authorization');
|
|
461
|
+
const user = await verifyToken(token);
|
|
462
|
+
|
|
463
|
+
// This object becomes the SOURCE for AuthorizedUserQuery resolvers
|
|
464
|
+
return {
|
|
465
|
+
_id: user._id,
|
|
466
|
+
username: user.username,
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
AuthorizedUserQuery: {
|
|
471
|
+
me: ([source]) => {
|
|
472
|
+
// source is what Query.user returned
|
|
473
|
+
const src = source as { _id: string; username: string };
|
|
474
|
+
return src;
|
|
475
|
+
},
|
|
476
|
+
todos: async ([source]) => {
|
|
477
|
+
// Access parent data
|
|
478
|
+
const src = source as { _id: string };
|
|
479
|
+
return getTodosByUserId(src._id);
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Typing the Parent (Two Methods)
|
|
486
|
+
|
|
487
|
+
**Method 1: Type Assertion (Simple)**
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
type UserSource = {
|
|
491
|
+
_id: string;
|
|
492
|
+
username: string;
|
|
493
|
+
token?: string;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
export default createResolvers({
|
|
497
|
+
AuthorizedUserQuery: {
|
|
498
|
+
me: ([source]) => {
|
|
499
|
+
const src = source as UserSource;
|
|
500
|
+
return {
|
|
501
|
+
_id: src._id,
|
|
502
|
+
username: src.username,
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Method 2: Using setSourceTypeFromResolver (Advanced)**
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
import { createResolvers, setSourceTypeFromResolver } from '@aexol/axolotl-core';
|
|
513
|
+
|
|
514
|
+
const getUserResolver = async ([, , context]) => {
|
|
515
|
+
const user = await authenticateUser(context);
|
|
516
|
+
return {
|
|
517
|
+
_id: user._id,
|
|
518
|
+
username: user.username,
|
|
519
|
+
email: user.email,
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const getUser = setSourceTypeFromResolver(getUserResolver);
|
|
524
|
+
|
|
525
|
+
export default createResolvers({
|
|
526
|
+
Query: {
|
|
527
|
+
user: getUserResolver,
|
|
528
|
+
},
|
|
529
|
+
AuthorizedUserQuery: {
|
|
530
|
+
me: ([source]) => {
|
|
531
|
+
const src = getUser(source); // src is now fully typed
|
|
532
|
+
return src;
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Organized Resolver Structure (Recommended)
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// src/resolvers/Query/resolvers.ts
|
|
542
|
+
import { createResolvers } from '../axolotl.js';
|
|
543
|
+
import user from './user.js';
|
|
544
|
+
|
|
545
|
+
export default createResolvers({
|
|
546
|
+
Query: {
|
|
547
|
+
...user.Query,
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// src/resolvers/Query/user.ts
|
|
552
|
+
import { createResolvers } from '../axolotl.js';
|
|
553
|
+
|
|
554
|
+
export default createResolvers({
|
|
555
|
+
Query: {
|
|
556
|
+
user: async ([, , context]) => {
|
|
557
|
+
// Return object to enable nested resolvers
|
|
558
|
+
return {};
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Main resolvers.ts
|
|
564
|
+
import { mergeAxolotls } from '@aexol/axolotl-core';
|
|
565
|
+
import QueryResolvers from '@/src/resolvers/Query/resolvers.js';
|
|
566
|
+
import MutationResolvers from '@/src/resolvers/Mutation/resolvers.js';
|
|
567
|
+
|
|
568
|
+
export default mergeAxolotls(QueryResolvers, MutationResolvers);
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Key Points:**
|
|
572
|
+
|
|
573
|
+
- Arguments are automatically typed from schema
|
|
574
|
+
- Return types must match schema definitions
|
|
575
|
+
- For nested resolvers, return an empty object `{}` in parent resolver
|
|
576
|
+
- Always use async functions (best practice)
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## STEP 6: Server Configuration
|
|
581
|
+
|
|
582
|
+
**File: src/index.ts**
|
|
583
|
+
|
|
584
|
+
### Basic Server
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import { adapter } from '@/src/axolotl.js';
|
|
588
|
+
import resolvers from '@/src/resolvers.js';
|
|
589
|
+
|
|
590
|
+
const { server, yoga } = adapter(
|
|
591
|
+
{ resolvers },
|
|
592
|
+
{
|
|
593
|
+
yoga: {
|
|
594
|
+
graphiql: true, // Enable GraphiQL UI
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
server.listen(4000, () => {
|
|
600
|
+
console.log('Server running on http://localhost:4000');
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### With Custom Scalars
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import { GraphQLScalarType, Kind } from 'graphql';
|
|
608
|
+
import { createScalars } from '@/src/axolotl.js';
|
|
609
|
+
|
|
610
|
+
const scalars = createScalars({
|
|
611
|
+
Secret: new GraphQLScalarType({
|
|
612
|
+
name: 'Secret',
|
|
613
|
+
serialize: (value) => String(value),
|
|
614
|
+
parseValue: (value) => Number(value),
|
|
615
|
+
parseLiteral: (ast) => {
|
|
616
|
+
if (ast.kind !== Kind.INT) return null;
|
|
617
|
+
return Number(ast.value);
|
|
618
|
+
},
|
|
619
|
+
}),
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
adapter({ resolvers, scalars });
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### With Directives
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import { createDirectives } from '@/src/axolotl.js';
|
|
629
|
+
import { MapperKind } from '@graphql-tools/utils';
|
|
630
|
+
import { defaultFieldResolver } from 'graphql';
|
|
631
|
+
|
|
632
|
+
const directives = createDirectives({
|
|
633
|
+
auth: (schema, getDirective) => ({
|
|
634
|
+
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
|
|
635
|
+
const hasDirective = getDirective(schema, fieldConfig, 'auth');
|
|
636
|
+
if (!hasDirective) return fieldConfig;
|
|
637
|
+
|
|
638
|
+
const { resolve = defaultFieldResolver } = fieldConfig as any;
|
|
639
|
+
return {
|
|
640
|
+
...fieldConfig,
|
|
641
|
+
resolve: async (source, args, context, info) => {
|
|
642
|
+
if (!context.userId) {
|
|
643
|
+
throw new Error('Not authenticated');
|
|
644
|
+
}
|
|
645
|
+
return resolve(source, args, context, info);
|
|
646
|
+
},
|
|
647
|
+
} as any;
|
|
648
|
+
},
|
|
649
|
+
}),
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
adapter({ resolvers, directives });
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## STEP 7: Micro-Federation (Optional)
|
|
658
|
+
|
|
659
|
+
**Purpose:** Merge multiple GraphQL schemas and resolvers into one API.
|
|
660
|
+
|
|
661
|
+
**Configuration in axolotl.json:**
|
|
662
|
+
|
|
663
|
+
```json
|
|
664
|
+
{
|
|
665
|
+
"schema": "schema.graphql",
|
|
666
|
+
"models": "src/models.ts",
|
|
667
|
+
"federation": [
|
|
668
|
+
{
|
|
669
|
+
"schema": "src/todos/schema.graphql",
|
|
670
|
+
"models": "src/todos/models.ts"
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
"schema": "src/users/schema.graphql",
|
|
674
|
+
"models": "src/users/models.ts"
|
|
675
|
+
}
|
|
676
|
+
]
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Each module has its own:**
|
|
681
|
+
|
|
682
|
+
- `schema.graphql`
|
|
683
|
+
- `models.ts` (generated)
|
|
684
|
+
- `axolotl.ts` (module-specific initialization)
|
|
685
|
+
- Resolvers
|
|
686
|
+
|
|
687
|
+
**Module axolotl.ts:**
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
// src/todos/axolotl.ts
|
|
691
|
+
import { Models } from '@/src/todos/models.js';
|
|
692
|
+
import { Axolotl } from '@aexol/axolotl-core';
|
|
693
|
+
import { graphqlYogaAdapter } from '@aexol/axolotl-graphql-yoga';
|
|
694
|
+
|
|
695
|
+
export const { createResolvers } = Axolotl(graphqlYogaAdapter)<Models>();
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**Main resolvers (merge):**
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// src/resolvers.ts
|
|
702
|
+
import { mergeAxolotls } from '@aexol/axolotl-core';
|
|
703
|
+
import todosResolvers from '@/src/todos/resolvers/resolvers.js';
|
|
704
|
+
import usersResolvers from '@/src/users/resolvers/resolvers.js';
|
|
705
|
+
|
|
706
|
+
export default mergeAxolotls(todosResolvers, usersResolvers);
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Key Points:**
|
|
710
|
+
|
|
711
|
+
- Run `axolotl build` to generate ALL models (main + federated)
|
|
712
|
+
- Each module has its own axolotl.ts using its own models
|
|
713
|
+
- Merge all resolvers using `mergeAxolotls()`
|
|
714
|
+
- Schema files are merged automatically by CLI
|
|
715
|
+
|
|
716
|
+
---
|
|
4
717
|
|
|
5
718
|
## Common Commands
|
|
6
719
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
720
|
+
```bash
|
|
721
|
+
# Create new Axolotl project with Yoga
|
|
722
|
+
npx @aexol/axolotl create-yoga my-project
|
|
723
|
+
|
|
724
|
+
# Generate models from schema
|
|
725
|
+
npx @aexol/axolotl build
|
|
726
|
+
|
|
727
|
+
# Generate models with custom directory
|
|
728
|
+
npx @aexol/axolotl build --cwd path/to/project
|
|
729
|
+
|
|
730
|
+
# Generate resolver boilerplate from @resolver directives
|
|
731
|
+
npx @aexol/axolotl resolvers
|
|
732
|
+
|
|
733
|
+
# Inspect resolvers (find missing/extra resolvers)
|
|
734
|
+
npx @aexol/axolotl inspect -s schema.graphql -r src/resolvers.ts
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## LLM Workflow Checklist
|
|
740
|
+
|
|
741
|
+
When working with an Axolotl project:
|
|
742
|
+
|
|
743
|
+
1. ✅ **Read axolotl.json** to understand structure
|
|
744
|
+
2. ✅ **Check schema.graphql** for current schema
|
|
745
|
+
3. ✅ **Verify models.ts is up-to-date** (regenerate if needed)
|
|
746
|
+
4. ✅ **Locate axolotl.ts** to understand initialization
|
|
747
|
+
5. ✅ **Find resolver files** and understand structure
|
|
748
|
+
6. ✅ **Make schema changes** if requested
|
|
749
|
+
7. ✅ **Run `axolotl build`** after schema changes
|
|
750
|
+
8. ✅ **Optionally run `axolotl resolvers`** to scaffold new resolver files
|
|
751
|
+
9. ✅ **Update resolvers** to match new types
|
|
752
|
+
10. ✅ **Test** that server starts without type errors
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## Common Patterns Cheat Sheet
|
|
757
|
+
|
|
758
|
+
### Context Type Safety
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
// ✅ CORRECT
|
|
762
|
+
type AppContext = YogaInitialContext & { userId: string };
|
|
763
|
+
|
|
764
|
+
graphqlYogaWithContextAdapter<AppContext>(async (initial) => ({
|
|
765
|
+
...initial,
|
|
766
|
+
userId: '123',
|
|
767
|
+
}));
|
|
768
|
+
|
|
769
|
+
// ❌ WRONG - Not extending YogaInitialContext
|
|
770
|
+
type AppContext = { userId: string };
|
|
771
|
+
|
|
772
|
+
// ❌ WRONG - Not spreading initial
|
|
773
|
+
graphqlYogaWithContextAdapter<AppContext>(async (initial) => ({
|
|
774
|
+
userId: '123', // Missing ...initial
|
|
775
|
+
}));
|
|
776
|
+
|
|
777
|
+
// ❌ WRONG - Passing object instead of function
|
|
778
|
+
graphqlYogaWithContextAdapter<AppContext>({ userId: '123' });
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Resolver Patterns
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
// Type-safe arguments (auto-typed from schema)
|
|
785
|
+
createResolvers({
|
|
786
|
+
Query: {
|
|
787
|
+
user: async ([, , context], { id, includeEmail }) => {
|
|
788
|
+
// id: string, includeEmail: boolean | undefined
|
|
789
|
+
return getUserById(id, includeEmail);
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Nested resolvers
|
|
795
|
+
createResolvers({
|
|
796
|
+
Query: {
|
|
797
|
+
user: async ([, , context]) => {
|
|
798
|
+
return {}; // Enable nested resolvers
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
UserQuery: {
|
|
802
|
+
me: async ([, , context]) => {
|
|
803
|
+
return getUserById(context.userId);
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## Troubleshooting
|
|
812
|
+
|
|
813
|
+
### Type errors in resolvers
|
|
814
|
+
|
|
815
|
+
**Solution:** Run `npx @aexol/axolotl build` to regenerate models
|
|
816
|
+
|
|
817
|
+
### Scalar types showing as 'unknown'
|
|
818
|
+
|
|
819
|
+
**Solution:** Map scalars in axolotl.ts:
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
Axolotl(adapter)<Models<{ MyScalar: string }>, Scalars>();
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### Context type not recognized
|
|
826
|
+
|
|
827
|
+
**Solution:** Use `graphqlYogaWithContextAdapter<YourContextType>(contextFunction)`
|
|
828
|
+
|
|
829
|
+
### Context properties undefined
|
|
15
830
|
|
|
16
|
-
|
|
831
|
+
**Solution:** Make sure you spread `...initial` when building context
|
|
17
832
|
|
|
18
|
-
|
|
833
|
+
---
|
|
19
834
|
|
|
20
|
-
|
|
21
|
-
- Local via npx: `npx @aexol/axolotl <command>`
|
|
835
|
+
## Quick Reference
|
|
22
836
|
|
|
23
|
-
|
|
837
|
+
| Task | Command/Code |
|
|
838
|
+
| -------------------- | ----------------------------------------------------------- |
|
|
839
|
+
| Initialize project | `npx @aexol/axolotl create-yoga <name>` |
|
|
840
|
+
| Generate types | `npx @aexol/axolotl build` |
|
|
841
|
+
| Scaffold resolvers | `npx @aexol/axolotl resolvers` |
|
|
842
|
+
| Create resolvers | `createResolvers({ Query: {...} })` |
|
|
843
|
+
| Access context | `([, , context])` - third in tuple |
|
|
844
|
+
| Access parent | `([source])` - first in tuple |
|
|
845
|
+
| Merge resolvers | `mergeAxolotls(resolvers1, resolvers2)` |
|
|
846
|
+
| Start server | `adapter({ resolvers }).server.listen(4000)` |
|
|
847
|
+
| Add custom context | `graphqlYogaWithContextAdapter<Ctx>(contextFn)` |
|
|
848
|
+
| Context must extend | `YogaInitialContext & { custom }` |
|
|
849
|
+
| Context must include | `{ ...initial, ...custom }` |
|
|
850
|
+
| Define scalars | `createScalars({ ScalarName: GraphQLScalarType })` |
|
|
851
|
+
| Define directives | `createDirectives({ directiveName: mapper })` |
|
|
852
|
+
| Inspect resolvers | `npx @aexol/axolotl inspect -s schema.graphql -r resolvers` |
|
|
24
853
|
|
|
25
|
-
|
|
854
|
+
---
|
|
26
855
|
|
|
27
|
-
|
|
28
|
-
- Test: `npm test`
|
|
29
|
-
- Lint: `npx eslint packages/cli`
|
|
856
|
+
This guide provides everything an LLM needs to work effectively with Axolotl projects, from understanding the structure to implementing resolvers with full type safety.
|