@ttoss/graphql-api 0.9.8 → 0.9.10
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 +208 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -90,6 +90,214 @@ import './modules/User/composer';
|
|
|
90
90
|
export { schemaComposer };
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
## Recommended Module Structure
|
|
94
|
+
|
|
95
|
+
As your GraphQL API grows, you'll encounter three recurring problems: circular imports between modules, relational fields hidden in resolver files (making TC files incomplete), and monolith resolver files with hundreds of lines. The structure below solves all three.
|
|
96
|
+
|
|
97
|
+
### Directory Structure
|
|
98
|
+
|
|
99
|
+
Each module has **TC files** at the root and a `resolvers/` folder with one file per GraphQL field:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
src/modules/
|
|
103
|
+
├── Meta/
|
|
104
|
+
│ ├── index.ts # side-effect imports of all resolver files
|
|
105
|
+
│ ├── MetaAdAccountTC.ts # pure shape — scalars + string type refs
|
|
106
|
+
│ ├── MetaCampaignTC.ts
|
|
107
|
+
│ └── resolvers/
|
|
108
|
+
│ ├── Query.metaAdAccount.ts # root query
|
|
109
|
+
│ ├── Mutation.addMetaAdAccounts.ts # root mutation
|
|
110
|
+
│ ├── MetaAdAccount.optimizations.ts # cross-module relational field
|
|
111
|
+
│ └── MetaCampaign.optimization.ts
|
|
112
|
+
├── Optimizations/
|
|
113
|
+
│ ├── index.ts
|
|
114
|
+
│ ├── OptimizationTC.ts
|
|
115
|
+
│ └── resolvers/
|
|
116
|
+
│ ├── Query.optimization.ts
|
|
117
|
+
│ ├── Mutation.createOptimization.ts
|
|
118
|
+
│ ├── Optimization.metaCampaign.ts # cross-module relational field
|
|
119
|
+
│ └── Optimization.algorithm.ts
|
|
120
|
+
└── User/
|
|
121
|
+
├── index.ts
|
|
122
|
+
├── UserTC.ts
|
|
123
|
+
└── resolvers/
|
|
124
|
+
└── Query.me.ts
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Rule 1: TC Files — All Fields Declared with String Type References
|
|
128
|
+
|
|
129
|
+
TC files declare **all** fields (scalars and relational) but use **string type names** for cross-module types instead of importing other TCs. `schemaComposer` resolves string type names lazily when building the schema, eliminating circular imports and keeping the full type shape visible in one file.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// MetaAdAccountTC.ts — no imports from other modules
|
|
133
|
+
import { schemaComposer } from '@ttoss/graphql-api';
|
|
134
|
+
|
|
135
|
+
export const MetaAdAccountTC = schemaComposer.createObjectTC({
|
|
136
|
+
name: 'MetaAdAccount',
|
|
137
|
+
fields: {
|
|
138
|
+
id: 'ID!',
|
|
139
|
+
name: 'String!',
|
|
140
|
+
// String type ref — resolved lazily by schemaComposer at build time
|
|
141
|
+
optimizations: '[Optimization!]!',
|
|
142
|
+
tracking: 'MetaAdAccountTracking',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Rule 2: Resolver Files — One Field Per File with `extendField`
|
|
148
|
+
|
|
149
|
+
Each resolver file attaches the `resolve` function to a single field using `extendField`. Root queries and mutations use `schemaComposer.Query.addFields()` / `schemaComposer.Mutation.addFields()`.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// resolvers/MetaAdAccount.optimizations.ts
|
|
153
|
+
import { schemaComposer } from '@ttoss/graphql-api';
|
|
154
|
+
import { MetaAdAccountTC } from '../MetaAdAccountTC';
|
|
155
|
+
|
|
156
|
+
MetaAdAccountTC.extendField('optimizations', {
|
|
157
|
+
args: { isActive: 'Boolean' },
|
|
158
|
+
resolve: async (source, args) => {
|
|
159
|
+
return findManyOptimizations({
|
|
160
|
+
metaAdAccountId: source.id,
|
|
161
|
+
isActive: args.isActive,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// resolvers/Query.metaAdAccount.ts
|
|
169
|
+
import { schemaComposer } from '@ttoss/graphql-api';
|
|
170
|
+
|
|
171
|
+
schemaComposer.Query.addFields({
|
|
172
|
+
metaAdAccount: {
|
|
173
|
+
type: 'MetaAdAccount',
|
|
174
|
+
args: { id: 'ID!' },
|
|
175
|
+
resolve: async (_source, args) => {
|
|
176
|
+
return findMetaAdAccount({ id: args.id });
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Rule 3: Dot Notation File Naming
|
|
183
|
+
|
|
184
|
+
Resolver files use `Parent.field.ts` naming that maps 1:1 to the GraphQL schema path:
|
|
185
|
+
|
|
186
|
+
| File name | GraphQL path |
|
|
187
|
+
| -------------------------------- | ---------------------------------------- |
|
|
188
|
+
| `Query.metaAdAccount.ts` | `metaAdAccount` field on `Query` |
|
|
189
|
+
| `Mutation.createOptimization.ts` | `createOptimization` field on `Mutation` |
|
|
190
|
+
| `MetaAdAccount.optimizations.ts` | `optimizations` field on `MetaAdAccount` |
|
|
191
|
+
|
|
192
|
+
### Rule 4: Index Files — Side-Effect Imports
|
|
193
|
+
|
|
194
|
+
Each module's `index.ts` imports all TC and resolver files as side effects, guaranteeing registration order:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Meta/index.ts
|
|
198
|
+
import './MetaAdAccountTC';
|
|
199
|
+
import './MetaCampaignTC';
|
|
200
|
+
import './resolvers/Query.metaAdAccount';
|
|
201
|
+
import './resolvers/Mutation.addMetaAdAccounts';
|
|
202
|
+
import './resolvers/MetaAdAccount.optimizations';
|
|
203
|
+
import './resolvers/MetaCampaign.optimization';
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The top-level `schemaComposer.ts` then imports each module index:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// src/schemaComposer.ts
|
|
210
|
+
import { schemaComposer } from '@ttoss/graphql-api';
|
|
211
|
+
import './modules/Meta';
|
|
212
|
+
import './modules/Optimizations';
|
|
213
|
+
import './modules/User';
|
|
214
|
+
|
|
215
|
+
export { schemaComposer };
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Why This Works
|
|
219
|
+
|
|
220
|
+
| Concern | Solution |
|
|
221
|
+
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
222
|
+
| **No circular imports** | TC files never import other module TCs. Only resolver files (leaf nodes) do cross-module imports, and nothing imports from them. |
|
|
223
|
+
| **Full shape visibility** | The TC file shows every field on the type — scalars and relational — in one place. |
|
|
224
|
+
| **Import order independence** | String type refs are resolved lazily at `buildSchema()` time. |
|
|
225
|
+
| **Easy to locate** | `Parent.field.ts` maps 1:1 to the GraphQL schema path. |
|
|
226
|
+
| **One responsibility per file** | Each resolver file handles exactly one field, keeping files small and focused. |
|
|
227
|
+
|
|
228
|
+
## Recommended Test Structure
|
|
229
|
+
|
|
230
|
+
Test files mirror the source layout using the same dot notation — one test file per resolver.
|
|
231
|
+
|
|
232
|
+
### Test Directory Structure
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
tests/unit/
|
|
236
|
+
├── Meta/
|
|
237
|
+
│ ├── Query.metaAdAccount.test.ts
|
|
238
|
+
│ ├── Query.metaAdAccounts.test.ts
|
|
239
|
+
│ ├── Mutation.addMetaAdAccounts.test.ts
|
|
240
|
+
│ ├── MetaAdAccount.optimizations.test.ts
|
|
241
|
+
│ └── MetaCampaign.optimization.test.ts
|
|
242
|
+
├── Optimizations/
|
|
243
|
+
│ ├── Query.optimization.test.ts
|
|
244
|
+
│ ├── Mutation.createOptimization.test.ts
|
|
245
|
+
│ └── Optimization.metaCampaign.test.ts
|
|
246
|
+
└── User/
|
|
247
|
+
└── Query.me.test.ts
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Test Structure Rules
|
|
251
|
+
|
|
252
|
+
1. **Same dot notation** as resolver files — `Parent.field.test.ts`
|
|
253
|
+
2. **Module subfolder** — `tests/unit/{Module}/` mirrors `src/modules/{Module}/resolvers/`
|
|
254
|
+
3. **No `resolvers/` subfolder in tests** — unnecessary nesting since test files are already leaf nodes
|
|
255
|
+
4. **One test file per resolver** — keeps tests focused and easy to find
|
|
256
|
+
|
|
257
|
+
### Testing Style
|
|
258
|
+
|
|
259
|
+
Prefer schema-level testing using `graphql()` against `schemaComposer.buildSchema()` — this validates the full wiring (TC shape → resolver → response):
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { graphql } from 'graphql';
|
|
263
|
+
import { schemaComposer } from 'src/schemaComposer';
|
|
264
|
+
|
|
265
|
+
test('should return optimizations for a MetaAdAccount', async () => {
|
|
266
|
+
const response = await graphql({
|
|
267
|
+
schema: schemaComposer.buildSchema(),
|
|
268
|
+
source: /* GraphQL */ `
|
|
269
|
+
query ($id: ID!) {
|
|
270
|
+
node(id: $id) {
|
|
271
|
+
... on MetaAdAccount {
|
|
272
|
+
optimizations {
|
|
273
|
+
id
|
|
274
|
+
isActive
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
`,
|
|
280
|
+
contextValue: mockContext,
|
|
281
|
+
variableValues: { id: globalId },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(response.errors).toBeUndefined();
|
|
285
|
+
expect(response.data).toEqual({
|
|
286
|
+
/* expected */
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Source-to-Test Mapping
|
|
292
|
+
|
|
293
|
+
| Source file | Test file |
|
|
294
|
+
| -------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
295
|
+
| `src/modules/Meta/resolvers/Query.metaAdAccount.ts` | `tests/unit/Meta/Query.metaAdAccount.test.ts` |
|
|
296
|
+
| `src/modules/Meta/resolvers/MetaAdAccount.optimizations.ts` | `tests/unit/Meta/MetaAdAccount.optimizations.test.ts` |
|
|
297
|
+
| `src/modules/Optimizations/resolvers/Mutation.createOptimization.ts` | `tests/unit/Optimizations/Mutation.createOptimization.test.ts` |
|
|
298
|
+
|
|
299
|
+
This 1:1 mapping makes it trivial to find the test for any resolver and vice versa.
|
|
300
|
+
|
|
93
301
|
## Relay Server Specification
|
|
94
302
|
|
|
95
303
|
As ttoss uses Relay as the main GraphQL client, this library implements the [Relay Server Specification](https://relay.dev/docs/guides/graphql-server-specification/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/graphql-api",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"description": "A library for building GraphQL APIs using ttoss ecosystem.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ttoss",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"graphql-middleware": "^6.1.35",
|
|
36
36
|
"graphql-shield": "^7.6.5",
|
|
37
37
|
"npmlog": "^7.0.1",
|
|
38
|
-
"@ttoss/ids": "^0.4.
|
|
38
|
+
"@ttoss/ids": "^0.4.9"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"graphql": "^16.6.0"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"graphql": "^16.11.0",
|
|
45
45
|
"jest": "^30.3.0",
|
|
46
46
|
"tsup": "^8.5.1",
|
|
47
|
-
"@ttoss/config": "^1.37.
|
|
47
|
+
"@ttoss/config": "^1.37.9"
|
|
48
48
|
},
|
|
49
49
|
"keywords": [
|
|
50
50
|
"api",
|