@trackunit/react-graphql-tools 1.16.4-alpha-3e7014314a8.0 → 1.17.3

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/CHANGELOG.md CHANGED
@@ -1,3 +1,84 @@
1
+ ## 1.17.3 (2026-08-27)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated iris-app-build-utilities to 2.4.23
6
+
7
+ ## 1.17.2 (2026-08-27)
8
+
9
+ ### 🧱 Updated Dependencies
10
+
11
+ - Updated iris-app-build-utilities to 2.4.22
12
+
13
+ ## 1.17.1 (2026-08-27)
14
+
15
+ ### 🧱 Updated Dependencies
16
+
17
+ - Updated iris-app-build-utilities to 2.4.21
18
+
19
+ ## 1.17.0 (2026-08-27)
20
+
21
+ ### 🚀 Features
22
+
23
+ - **react-graphql-tools:** derive the authorization scopes a project requires ([7c5c1db1258](https://github.com/Trackunit/manager/commit/7c5c1db1258))
24
+
25
+ ### 🧱 Updated Dependencies
26
+
27
+ - Updated iris-app-build-utilities to 2.4.20
28
+
29
+ ### ❤️ Thank You
30
+
31
+ - Cursor @cursoragent
32
+ - Mikkel Thorbjørn Andersen
33
+
34
+ ## 1.16.3 (2026-08-27)
35
+
36
+ ### 🧱 Updated Dependencies
37
+
38
+ - Updated iris-app-build-utilities to 2.4.19
39
+
40
+ ## 1.16.2 (2026-08-25)
41
+
42
+ ### 🩹 Fixes
43
+
44
+ - **react-graphql-tools:** repair scope collection after applied review suggestions ([3890f3a9706](https://github.com/Trackunit/manager/commit/3890f3a9706))
45
+
46
+ ### 🧱 Updated Dependencies
47
+
48
+ - Updated iris-app-build-utilities to 2.4.18
49
+ - Updated react-vite-test-setup to 0.0.114
50
+
51
+ ### ❤️ Thank You
52
+
53
+ - Cursor @cursoragent
54
+ - Mikkel Thorbjørn Andersen
55
+
56
+ ## 1.16.1 (2026-08-25)
57
+
58
+ ### 🧱 Updated Dependencies
59
+
60
+ - Updated iris-app-build-utilities to 2.4.17
61
+ - Updated react-vite-test-setup to 0.0.113
62
+
63
+ ## 1.16.0 (2026-08-24)
64
+
65
+ ### 🚀 Features
66
+
67
+ - **react-graphql-tools:** document required authorization scopes in generated mocks ([8b500687a87](https://github.com/Trackunit/manager/commit/8b500687a87))
68
+
69
+ ### 🩹 Fixes
70
+
71
+ - **react-graphql-tools:** repair scope collection after applied review suggestions ([e0fe4fad522](https://github.com/Trackunit/manager/commit/e0fe4fad522))
72
+
73
+ ### 🧱 Updated Dependencies
74
+
75
+ - Updated iris-app-build-utilities to 2.4.16
76
+
77
+ ### ❤️ Thank You
78
+
79
+ - Cursor @cursoragent
80
+ - Mikkel Thorbjørn Andersen
81
+
1
82
  ## 1.15.17 (2026-08-20)
2
83
 
3
84
  ### 🧱 Updated Dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-graphql-tools",
3
- "version": "1.16.4-alpha-3e7014314a8.0",
3
+ "version": "1.17.3",
4
4
  "main": "src/index.js",
5
5
  "executors": "./executors.json",
6
6
  "generators": "./generators.json",
@@ -18,7 +18,9 @@
18
18
  "win-ca": "^3.5.1",
19
19
  "tslib": "^2.6.2",
20
20
  "@nx/devkit": "23.1.0",
21
- "@nx/js": "23.1.0"
21
+ "@nx/js": "23.1.0",
22
+ "@trackunit/iris-app-build-utilities": "2.4.23",
23
+ "graphql": "^16.10.0"
22
24
  },
23
25
  "migrations": "./migrations.json",
24
26
  "types": "./src/index.d.ts",
@@ -0,0 +1,25 @@
1
+ import { GraphQLSchema } from "graphql";
2
+ /**
3
+ * Loads the authorization-annotated schema, reusing codegen's own schema resolution
4
+ * (`resolveSchema`) so scopes are read from the exact schema operations are generated against - the
5
+ * internal superset (which carries every `@authorization` directive) when checked out, otherwise
6
+ * the public schema. Returns null when only the remote endpoint is available (nothing to read from
7
+ * disk) or the schema file is missing (e.g. isolated test workspaces).
8
+ */
9
+ export declare const loadAuthSchema: (nxRoot: string) => GraphQLSchema | null;
10
+ /**
11
+ * Builds a map of operation name -> sorted, de-duplicated authorization scopes required by that
12
+ * operation, from raw GraphQL operation/fragment source documents.
13
+ *
14
+ * @param schema The authorization-annotated schema to resolve field directives against.
15
+ * @param documents Raw `.graphql` source strings (operations and any fragments they spread).
16
+ */
17
+ export declare const collectPermissionsFromDocuments: (schema: GraphQLSchema, documents: Array<string>) => Map<string, Array<string>>;
18
+ /**
19
+ * Builds a map of operation name -> sorted, de-duplicated authorization scopes required by that
20
+ * operation, based on the `@authorization` directives of every field the operation touches.
21
+ *
22
+ * @param schema The authorization-annotated schema to resolve field directives against.
23
+ * @param generatedFilePath Path to the generated `graphql.ts`; its lib `src` root holds the operations.
24
+ */
25
+ export declare const collectOperationPermissions: (schema: GraphQLSchema, generatedFilePath: string) => Map<string, Array<string>>;
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectOperationPermissions = exports.collectPermissionsFromDocuments = exports.loadAuthSchema = void 0;
4
+ const iris_app_build_utilities_1 = require("@trackunit/iris-app-build-utilities");
5
+ const fs_1 = require("fs");
6
+ const graphql_1 = require("graphql");
7
+ const path_1 = require("path");
8
+ const AUTH_DIRECTIVE = "authorization";
9
+ const ALLOW_ARGUMENT = "allow";
10
+ /**
11
+ * Schemas are large (tens of thousands of lines) and every generated lib triggers a scrub, so cache
12
+ * the built schema per absolute path for the lifetime of the process.
13
+ */
14
+ const schemaCache = new Map();
15
+ /**
16
+ * Loads the authorization-annotated schema, reusing codegen's own schema resolution
17
+ * (`resolveSchema`) so scopes are read from the exact schema operations are generated against - the
18
+ * internal superset (which carries every `@authorization` directive) when checked out, otherwise
19
+ * the public schema. Returns null when only the remote endpoint is available (nothing to read from
20
+ * disk) or the schema file is missing (e.g. isolated test workspaces).
21
+ */
22
+ const loadAuthSchema = (nxRoot) => {
23
+ const schemaPointer = (0, iris_app_build_utilities_1.resolveSchema)("auto");
24
+ // `resolveSchema` returns the public endpoint URL when no schema file is checked out; we can only
25
+ // inspect directives from a schema we can read off disk, so skip in that case.
26
+ if (/^https?:\/\//.test(schemaPointer))
27
+ return null;
28
+ const schemaPath = (0, path_1.isAbsolute)(schemaPointer) ? schemaPointer : (0, path_1.join)(nxRoot, schemaPointer);
29
+ if (!(0, fs_1.existsSync)(schemaPath))
30
+ return null;
31
+ const cached = schemaCache.get(schemaPath);
32
+ if (cached !== undefined)
33
+ return cached;
34
+ try {
35
+ const schema = (0, graphql_1.buildSchema)((0, fs_1.readFileSync)(schemaPath, { encoding: "utf-8" }), { assumeValidSDL: true });
36
+ schemaCache.set(schemaPath, schema);
37
+ return schema;
38
+ }
39
+ catch {
40
+ schemaCache.set(schemaPath, null);
41
+ return null;
42
+ }
43
+ };
44
+ exports.loadAuthSchema = loadAuthSchema;
45
+ /**
46
+ * Flattens a GraphQL value node into the string scopes it contains.
47
+ * `allow` accepts both a single string (`allow: "asset.view"`) and a list (`allow: ["asset.view"]`).
48
+ */
49
+ const valueToScopes = (value) => {
50
+ if (value.kind === "StringValue")
51
+ return [value.value];
52
+ if (value.kind === "ListValue")
53
+ return value.values.flatMap(valueToScopes);
54
+ return [];
55
+ };
56
+ /**
57
+ * Reads the scopes declared by any `@authorization(allow: ...)` directive in the given list.
58
+ */
59
+ const scopesFromDirectives = (directives) => {
60
+ if (!directives)
61
+ return [];
62
+ return directives
63
+ .filter(directive => directive.name.value === AUTH_DIRECTIVE)
64
+ .flatMap(directive => {
65
+ const allowArgument = directive.arguments?.find(argument => argument.name.value === ALLOW_ARGUMENT);
66
+ return allowArgument ? valueToScopes(allowArgument.value) : [];
67
+ });
68
+ };
69
+ /**
70
+ * Collects the object-level `@authorization` scopes declared directly on a type definition
71
+ * (the directive is valid on both FIELD_DEFINITION and OBJECT).
72
+ */
73
+ const scopesFromType = (type) => {
74
+ const definitionDirectives = scopesFromDirectives(type.astNode?.directives);
75
+ const extensionDirectives = type.extensionASTNodes.flatMap(extension => scopesFromDirectives(extension.directives));
76
+ return [...definitionDirectives, ...extensionDirectives];
77
+ };
78
+ /**
79
+ * Walks a selection set against the schema, following inline fragments and fragment spreads, and
80
+ * accumulates every authorization scope referenced by the accessed fields and their return types.
81
+ */
82
+ const collectSelectionScopes = (schema, parentType, selectionSet, fragments, scopes, visitedFragments) => {
83
+ if (!parentType)
84
+ return;
85
+ for (const selection of selectionSet.selections) {
86
+ if (selection.kind === "Field") {
87
+ if (selection.name.value === "__typename")
88
+ continue;
89
+ if (!(0, graphql_1.isObjectType)(parentType) && !(0, graphql_1.isInterfaceType)(parentType))
90
+ continue;
91
+ const fieldDefinition = parentType.getFields()[selection.name.value];
92
+ if (!fieldDefinition)
93
+ continue;
94
+ scopesFromDirectives(fieldDefinition.astNode?.directives).forEach(scope => scopes.add(scope));
95
+ const fieldType = (0, graphql_1.getNamedType)(fieldDefinition.type);
96
+ scopesFromType(fieldType).forEach(scope => scopes.add(scope));
97
+ if (selection.selectionSet) {
98
+ collectSelectionScopes(schema, fieldType, selection.selectionSet, fragments, scopes, visitedFragments);
99
+ }
100
+ }
101
+ else if (selection.kind === "InlineFragment") {
102
+ const targetType = selection.typeCondition ? schema.getType(selection.typeCondition.name.value) : parentType;
103
+ if (targetType)
104
+ scopesFromType(targetType).forEach(scope => scopes.add(scope));
105
+ collectSelectionScopes(schema, targetType ?? undefined, selection.selectionSet, fragments, scopes, visitedFragments);
106
+ }
107
+ else {
108
+ const fragmentName = selection.name.value;
109
+ if (visitedFragments.has(fragmentName))
110
+ continue;
111
+ visitedFragments.add(fragmentName);
112
+ const fragment = fragments.get(fragmentName);
113
+ if (!fragment)
114
+ continue;
115
+ const targetType = schema.getType(fragment.typeCondition.name.value);
116
+ if (targetType)
117
+ scopesFromType(targetType).forEach(scope => scopes.add(scope));
118
+ collectSelectionScopes(schema, targetType ?? undefined, fragment.selectionSet, fragments, scopes, visitedFragments);
119
+ }
120
+ }
121
+ };
122
+ /**
123
+ * Recursively reads every `.graphql` source document under a directory, skipping generated output.
124
+ * `*-manager.graphql` is excluded to match codegen's default document globs, so an operation that was
125
+ * never generated can't shadow a generated one of the same name.
126
+ */
127
+ const readGraphqlDocuments = (directory) => {
128
+ if (!(0, fs_1.existsSync)(directory))
129
+ return [];
130
+ const documents = [];
131
+ for (const entry of (0, fs_1.readdirSync)(directory, { withFileTypes: true })) {
132
+ const fullPath = (0, path_1.join)(directory, entry.name);
133
+ if (entry.isDirectory()) {
134
+ if (entry.name === "generated" || entry.name === "node_modules")
135
+ continue;
136
+ documents.push(...readGraphqlDocuments(fullPath));
137
+ }
138
+ else if (entry.name.endsWith(".graphql") && !entry.name.endsWith("-manager.graphql")) {
139
+ documents.push((0, fs_1.readFileSync)(fullPath, { encoding: "utf-8" }));
140
+ }
141
+ }
142
+ return documents;
143
+ };
144
+ /**
145
+ * Builds a map of operation name -> sorted, de-duplicated authorization scopes required by that
146
+ * operation, from raw GraphQL operation/fragment source documents.
147
+ *
148
+ * @param schema The authorization-annotated schema to resolve field directives against.
149
+ * @param documents Raw `.graphql` source strings (operations and any fragments they spread).
150
+ */
151
+ const collectPermissionsFromDocuments = (schema, documents) => {
152
+ const result = new Map();
153
+ if (documents.length === 0)
154
+ return result;
155
+ const definitions = [];
156
+ for (const document of documents) {
157
+ try {
158
+ definitions.push(...(0, graphql_1.parse)(document).definitions);
159
+ }
160
+ catch (error) {
161
+ // A single unparseable document must not silently strip scope docs from every other operation.
162
+ // eslint-disable-next-line no-console
163
+ console.warn(`[graphql-mock] Skipping unparseable GraphQL document: ${error.message}`);
164
+ }
165
+ }
166
+ const fragments = new Map();
167
+ for (const definition of definitions) {
168
+ if (definition.kind === "FragmentDefinition") {
169
+ fragments.set(definition.name.value, definition);
170
+ }
171
+ }
172
+ for (const definition of definitions) {
173
+ if (definition.kind !== "OperationDefinition" || !definition.name)
174
+ continue;
175
+ const rootType = definition.operation === "query"
176
+ ? schema.getQueryType()
177
+ : definition.operation === "mutation"
178
+ ? schema.getMutationType()
179
+ : schema.getSubscriptionType();
180
+ const scopes = new Set();
181
+ collectSelectionScopes(schema, rootType ?? undefined, definition.selectionSet, fragments, scopes, new Set());
182
+ result.set(definition.name.value, [...scopes].sort());
183
+ }
184
+ return result;
185
+ };
186
+ exports.collectPermissionsFromDocuments = collectPermissionsFromDocuments;
187
+ /**
188
+ * Builds a map of operation name -> sorted, de-duplicated authorization scopes required by that
189
+ * operation, based on the `@authorization` directives of every field the operation touches.
190
+ *
191
+ * @param schema The authorization-annotated schema to resolve field directives against.
192
+ * @param generatedFilePath Path to the generated `graphql.ts`; its lib `src` root holds the operations.
193
+ */
194
+ const collectOperationPermissions = (schema, generatedFilePath) => {
195
+ // <lib>/src/generated/graphql-api/graphql.ts -> <lib>/src
196
+ const documentsDirectory = (0, path_1.join)((0, path_1.dirname)(generatedFilePath), "..", "..");
197
+ return (0, exports.collectPermissionsFromDocuments)(schema, readGraphqlDocuments(documentsDirectory));
198
+ };
199
+ exports.collectOperationPermissions = collectOperationPermissions;
200
+ //# sourceMappingURL=collectPermissions.js.map
@@ -93,7 +93,7 @@ const scrubFile = async (file, nxRoot, isVerbose, generateMocks = true) => {
93
93
  // mutates the shared source file via insertStatements().
94
94
  const scrubbedFileContent = await (0, exports.scrubFileContent)(fileContent, nxRoot, isVerbose, sharedSourceFile);
95
95
  const scrubbedMockFileContent = generateMocks
96
- ? await (0, scrubMockFile_1.scrubMockFileContent)(fileContent, nxRoot, isVerbose, sharedSourceFile)
96
+ ? await (0, scrubMockFile_1.scrubMockFileContent)(fileContent, nxRoot, isVerbose, sharedSourceFile, file)
97
97
  : undefined;
98
98
  const prettierOptions = await getPrettierOptions(nxRoot);
99
99
  let prettySrc = scrubbedFileContent;
@@ -2,4 +2,4 @@ import { SourceFile } from "ts-morph";
2
2
  /**
3
3
  * Generates React hooks from your graphql files.
4
4
  */
5
- export declare const scrubMockFileContent: (fileContent: string, _nxRoot: string, isVerbose?: boolean, providedSourceFile?: SourceFile) => Promise<string>;
5
+ export declare const scrubMockFileContent: (fileContent: string, nxRoot: string, isVerbose?: boolean, providedSourceFile?: SourceFile, generatedFilePath?: string) => Promise<string>;
@@ -4,6 +4,7 @@ exports.scrubMockFileContent = void 0;
4
4
  /* eslint-disable no-console */
5
5
  const faker_1 = require("@faker-js/faker");
6
6
  const ts_morph_1 = require("ts-morph");
7
+ const collectPermissions_1 = require("./collectPermissions");
7
8
  const UNDEFINED_SENTINEL = "__UNDEFINED__";
8
9
  /**
9
10
  * Strips surrounding double-quotes from a ts-morph property name.
@@ -281,7 +282,7 @@ const generateMockFromType = (type, text) => {
281
282
  /**
282
283
  * Generates React hooks from your graphql files.
283
284
  */
284
- const scrubMockFileContent = async (fileContent, _nxRoot, isVerbose, providedSourceFile) => {
285
+ const scrubMockFileContent = async (fileContent, nxRoot, isVerbose, providedSourceFile, generatedFilePath) => {
285
286
  const start = new Date().getTime();
286
287
  const generatedFile = providedSourceFile ??
287
288
  new ts_morph_1.Project({
@@ -307,6 +308,10 @@ const scrubMockFileContent = async (fileContent, _nxRoot, isVerbose, providedSou
307
308
  const time = end - start;
308
309
  console.log(`Mock Execution time: ${time}`);
309
310
  }
311
+ // Resolve which authorization scopes each mocked operation requires. `@authorization(allow: [...])`
312
+ // directives only live in the schema, so we walk each operation's selection set against it.
313
+ const schema = generatedFilePath ? (0, collectPermissions_1.loadAuthSchema)(nxRoot) : null;
314
+ const permissionsByOperation = schema && generatedFilePath ? (0, collectPermissions_1.collectOperationPermissions)(schema, generatedFilePath) : new Map();
310
315
  const mock = mockThese.map(mocker => {
311
316
  const postFix = (mocker.getName().includes("Query") ? "Query" : "Mutation").length;
312
317
  const variablesTypeName = `${mocker.getName()}Variables`;
@@ -346,7 +351,15 @@ const scrubMockFileContent = async (fileContent, _nxRoot, isVerbose, providedSou
346
351
  : `
347
352
  const default${mocker.getName()}Variables: gql.${variablesTypeName} = ${defaultVarsSource};
348
353
  `;
349
- return `${constBlock}
354
+ const permissions = permissionsByOperation.get(docName) ?? [];
355
+ const permissionsComment = permissions.length > 0
356
+ ? `
357
+ /**
358
+ * Required authorization scopes (the token needs at least one allowed scope per accessed field):
359
+ ${permissions.map(scope => ` * - ${scope}`).join("\n")}
360
+ */`
361
+ : "";
362
+ return `${constBlock}${permissionsComment}
350
363
  export const mockFor${mocker.getName()} = (${variablesParam}, data?: DeepPartialNullable<gql.${mocker.getName()}> ) => {
351
364
  return queryFor(gql.${docName}Document, mergeDeepVars(${defaultVarsRef}, variables),
352
365
  mergeDeep(
@@ -355,6 +368,11 @@ const scrubMockFileContent = async (fileContent, _nxRoot, isVerbose, providedSou
355
368
  );
356
369
  }`;
357
370
  });
371
+ const mockedOperationNames = mockThese.map(mocker => {
372
+ const postFix = (mocker.getName().includes("Query") ? "Query" : "Mutation").length;
373
+ return mocker.getName().substring(0, mocker.getName().length - postFix);
374
+ });
375
+ logRequiredPermissions(mockedOperationNames, permissionsByOperation, generatedFilePath, isVerbose);
358
376
  return `
359
377
  import { mergeDeep } from "@apollo/client/utilities";
360
378
  import { mergeDeepVars, queryFor, DeepPartialNullable, PickDeepPartialNullable } from "@trackunit/react-core-contexts-test";
@@ -364,4 +382,21 @@ const scrubMockFileContent = async (fileContent, _nxRoot, isVerbose, providedSou
364
382
  `;
365
383
  };
366
384
  exports.scrubMockFileContent = scrubMockFileContent;
385
+ /**
386
+ * Prints a summary of the authorization scopes required by the mocked operations, plus the full
387
+ * de-duplicated list of scopes a token must be granted to satisfy every mock in the file.
388
+ */
389
+ const logRequiredPermissions = (operationNames, permissionsByOperation, generatedFilePath, isVerbose) => {
390
+ if (!isVerbose || permissionsByOperation.size === 0)
391
+ return;
392
+ const allScopes = new Set();
393
+ console.log(`[graphql-mock] Required authorization scopes${generatedFilePath ? ` for ${generatedFilePath}` : ""}:`);
394
+ operationNames.forEach(operationName => {
395
+ const scopes = permissionsByOperation.get(operationName) ?? [];
396
+ scopes.forEach(scope => allScopes.add(scope));
397
+ console.log(` ${operationName}: ${scopes.length > 0 ? scopes.join(", ") : "(none)"}`);
398
+ });
399
+ const sortedScopes = [...allScopes].sort();
400
+ console.log(`[graphql-mock] All scopes referenced (${sortedScopes.length}): ${sortedScopes.length > 0 ? sortedScopes.join(", ") : "(none)"}`);
401
+ };
367
402
  //# sourceMappingURL=scrubMockFile.js.map