@ttoss/graphql-api 0.9.8 → 0.9.9

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 (2) hide show
  1. package/README.md +208 -0
  2. package/package.json +1 -1
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.8",
3
+ "version": "0.9.9",
4
4
  "description": "A library for building GraphQL APIs using ttoss ecosystem.",
5
5
  "license": "MIT",
6
6
  "author": "ttoss",