@trackunit/react-graphql-tools 1.11.78 → 1.12.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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## 1.12.1 (2026-03-20)
2
+
3
+ ### 🧱 Updated Dependencies
4
+
5
+ - Updated react-test-setup to 1.8.76
6
+
7
+ ## 1.12.0 (2026-03-19)
8
+
9
+ ### 🚀 Features
10
+
11
+ - improve GraphQL mock generation for tests ([#22330](https://github.com/Trackunit/manager/pull/22330))
12
+
13
+ ### ❤️ Thank You
14
+
15
+ - Kieran Prince @kpr-trackunit
16
+
1
17
  ## 1.11.78 (2026-03-19)
2
18
 
3
19
  ### 🧱 Updated Dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-graphql-tools",
3
- "version": "1.11.78",
3
+ "version": "1.12.1",
4
4
  "main": "src/index.js",
5
5
  "executors": "./executors.json",
6
6
  "generators": "./generators.json",
@@ -10,14 +10,14 @@
10
10
  "node": ">=24.x"
11
11
  },
12
12
  "dependencies": {
13
- "@nx/devkit": "22.4.4",
14
13
  "@graphql-codegen/cli": "^5.0.3",
15
14
  "@graphql-codegen/client-preset": "^4.5.1",
16
15
  "prettier": "^3.4.2",
17
16
  "ts-morph": "^20.0.0",
18
17
  "@faker-js/faker": "^8.4.1",
19
18
  "win-ca": "^3.5.1",
20
- "tslib": "^2.6.2"
19
+ "tslib": "^2.6.2",
20
+ "@nx/devkit": "22.4.4"
21
21
  },
22
22
  "types": "./src/index.d.ts",
23
23
  "type": "commonjs"
@@ -4,6 +4,215 @@ 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 UNDEFINED_SENTINEL = "__UNDEFINED__";
8
+ /**
9
+ * Strips surrounding double-quotes from a ts-morph property name.
10
+ * Codegen may emit JSON-style keys (`"kind"`) whose getName() includes the quotes.
11
+ */
12
+ const stripQuotes = (name) => (name.startsWith('"') && name.endsWith('"') ? name.slice(1, -1) : name);
13
+ /**
14
+ * Finds a property in an object literal by name, handling both quoted and unquoted keys.
15
+ */
16
+ const findProperty = (obj, name) => obj.getProperty(name) ?? obj.getProperty(`"${name}"`);
17
+ /**
18
+ * Resolves the type of a property symbol in the context of a source file.
19
+ * Uses the TypeChecker to correctly handle mapped types like Exact<{...}>.
20
+ */
21
+ const getPropertyType = (prop, sourceFile) => {
22
+ return sourceFile.getProject().getTypeChecker().getTypeOfSymbolAtLocation(prop, sourceFile);
23
+ };
24
+ /**
25
+ * Checks whether a type is an InputMaybe union (contains null or undefined members).
26
+ * Required unions like `string | string[]` or enums do NOT match.
27
+ */
28
+ const isInputMaybe = (type) => type.isUnion() && type.getUnionTypes().some(m => m.isNull() || m.isUndefined());
29
+ /**
30
+ * Returns the names of required variable fields (those whose type is NOT
31
+ * wrapped in InputMaybe, i.e. not a union containing null/undefined).
32
+ */
33
+ const getRequiredVariableKeys = (variablesType, sourceFile) => {
34
+ return variablesType
35
+ .getProperties()
36
+ .filter(prop => !isInputMaybe(getPropertyType(prop, sourceFile)))
37
+ .map(prop => prop.getName());
38
+ };
39
+ /**
40
+ * Generates default variable values for GraphQL query/mutation variables.
41
+ *
42
+ * - InputMaybe<T> (union with null/undefined) -> UNDEFINED_SENTINEL
43
+ * - Required union (e.g. string | string[], enums) -> faker value from first concrete member
44
+ * - Required scalar -> faker value
45
+ * - Required object -> recursively expanded
46
+ */
47
+ const generateDefaultFromInputType = (type, sourceFile, depth = 0) => {
48
+ if (depth > 10)
49
+ return UNDEFINED_SENTINEL;
50
+ if (isInputMaybe(type)) {
51
+ return UNDEFINED_SENTINEL;
52
+ }
53
+ if (type.isUnion()) {
54
+ const firstMember = type.getUnionTypes()[0];
55
+ if (!firstMember)
56
+ return UNDEFINED_SENTINEL;
57
+ return generateDefaultFromInputType(firstMember, sourceFile, depth);
58
+ }
59
+ if (type.isArray()) {
60
+ const elementType = type.getArrayElementType();
61
+ if (!elementType)
62
+ return UNDEFINED_SENTINEL;
63
+ const elementDefault = generateDefaultFromInputType(elementType, sourceFile, depth + 1);
64
+ if (elementDefault === UNDEFINED_SENTINEL)
65
+ return [];
66
+ return [elementDefault];
67
+ }
68
+ if (type.isObject()) {
69
+ const properties = type.getProperties();
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ const obj = {};
72
+ for (const prop of properties) {
73
+ const propType = getPropertyType(prop, sourceFile);
74
+ obj[prop.getName()] = generateDefaultFromInputType(propType, sourceFile, depth + 1);
75
+ }
76
+ return obj;
77
+ }
78
+ if (type.isStringLiteral())
79
+ return type.getLiteralValue();
80
+ if (type.isNumberLiteral())
81
+ return type.getLiteralValue();
82
+ if (type.isBooleanLiteral())
83
+ return type.getText() === "true";
84
+ const typeText = type.getText();
85
+ if (typeText === "string" || typeText === "number" || typeText === "boolean") {
86
+ faker_1.faker.seed(123);
87
+ if (typeText === "string")
88
+ return faker_1.faker.lorem.word();
89
+ if (typeText === "number")
90
+ return faker_1.faker.number.int();
91
+ return false;
92
+ }
93
+ return UNDEFINED_SENTINEL;
94
+ };
95
+ const parseGraphqlDefaultValue = (kind, rawValue) => {
96
+ switch (kind) {
97
+ case "BooleanValue":
98
+ return rawValue === "true";
99
+ case "IntValue":
100
+ return rawValue ? parseInt(rawValue.replace(/"/g, ""), 10) : 0;
101
+ case "FloatValue":
102
+ return rawValue ? parseFloat(rawValue.replace(/"/g, "")) : 0;
103
+ case "StringValue":
104
+ return rawValue ? rawValue.replace(/^"|"$/g, "") : "";
105
+ case "EnumValue":
106
+ return rawValue ? rawValue.replace(/^"|"$/g, "") : undefined;
107
+ case "NullValue":
108
+ return null;
109
+ default:
110
+ return undefined;
111
+ }
112
+ };
113
+ /**
114
+ * Navigates variable -> name -> value inside a VariableDefinition object literal
115
+ * to extract the GraphQL variable name.
116
+ */
117
+ const extractVarDefName = (obj) => {
118
+ const varProp = obj
119
+ .getChildrenOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)
120
+ .find(p => stripQuotes(p.getName()) === "variable");
121
+ if (!varProp)
122
+ return undefined;
123
+ const varInit = varProp.getInitializer();
124
+ if (!varInit || !ts_morph_1.Node.isObjectLiteralExpression(varInit))
125
+ return undefined;
126
+ const namePropInner = varInit
127
+ .getChildrenOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)
128
+ .find(p => stripQuotes(p.getName()) === "name");
129
+ if (!namePropInner)
130
+ return undefined;
131
+ const nameInit = namePropInner.getInitializer();
132
+ if (!nameInit || !ts_morph_1.Node.isObjectLiteralExpression(nameInit))
133
+ return undefined;
134
+ const valuePropInner = nameInit
135
+ .getChildrenOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)
136
+ .find(p => stripQuotes(p.getName()) === "value");
137
+ if (!valuePropInner)
138
+ return undefined;
139
+ return valuePropInner.getInitializer()?.getText().replace(/"/g, "");
140
+ };
141
+ /**
142
+ * Extracts GraphQL-level default values from the DocumentNode AST constant.
143
+ * e.g. `$withValues: Boolean = true` produces
144
+ * `defaultValue: { kind: "BooleanValue", value: true }` in the generated TypeScript.
145
+ */
146
+ const extractGraphqlDefaultValues = (sourceFile, documentName) => {
147
+ const defaults = {};
148
+ const decl = sourceFile.getVariableDeclaration(documentName);
149
+ if (!decl)
150
+ return defaults;
151
+ const objectLiterals = decl.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
152
+ for (const obj of objectLiterals) {
153
+ const kindProp = findProperty(obj, "kind");
154
+ if (!kindProp || !ts_morph_1.Node.isPropertyAssignment(kindProp))
155
+ continue;
156
+ if (kindProp.getInitializer()?.getText() !== '"VariableDefinition"')
157
+ continue;
158
+ const dvProp = findProperty(obj, "defaultValue");
159
+ if (!dvProp || !ts_morph_1.Node.isPropertyAssignment(dvProp))
160
+ continue;
161
+ const dvInit = dvProp.getInitializer();
162
+ if (!dvInit || !ts_morph_1.Node.isObjectLiteralExpression(dvInit))
163
+ continue;
164
+ const varName = extractVarDefName(obj);
165
+ if (!varName)
166
+ continue;
167
+ const dvKindProp = findProperty(dvInit, "kind");
168
+ if (!dvKindProp || !ts_morph_1.Node.isPropertyAssignment(dvKindProp))
169
+ continue;
170
+ const dvKind = dvKindProp.getInitializer()?.getText().replace(/"/g, "");
171
+ let dvValue;
172
+ const dvValueProp = findProperty(dvInit, "value");
173
+ if (dvValueProp && ts_morph_1.Node.isPropertyAssignment(dvValueProp)) {
174
+ dvValue = dvValueProp.getInitializer()?.getText();
175
+ }
176
+ const parsed = parseGraphqlDefaultValue(dvKind, dvValue);
177
+ if (parsed !== undefined) {
178
+ defaults[varName] = parsed;
179
+ }
180
+ }
181
+ return defaults;
182
+ };
183
+ /**
184
+ * Serializes a value to JavaScript source code, emitting `undefined` for sentinel values.
185
+ * JSON.stringify drops undefined, so we need a custom serializer.
186
+ */
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ const serializeToSource = (value, indent = 0) => {
189
+ const pad = " ".repeat(indent);
190
+ const innerPad = " ".repeat(indent + 4);
191
+ if (value === UNDEFINED_SENTINEL)
192
+ return "undefined";
193
+ if (value === null)
194
+ return "null";
195
+ if (typeof value === "string")
196
+ return JSON.stringify(value);
197
+ if (typeof value === "number" || typeof value === "boolean")
198
+ return String(value);
199
+ if (Array.isArray(value)) {
200
+ if (value.length === 0)
201
+ return "[]";
202
+ const items = value.map(v => `${innerPad}${serializeToSource(v, indent + 4)}`).join(",\n");
203
+ return `[\n${items}\n${pad}]`;
204
+ }
205
+ if (typeof value === "object") {
206
+ const entries = Object.entries(value);
207
+ if (entries.length === 0)
208
+ return "{}";
209
+ const props = entries
210
+ .map(([k, v]) => `${innerPad}${JSON.stringify(k)}: ${serializeToSource(v, indent + 4)}`)
211
+ .join(",\n");
212
+ return `{\n${props}\n${pad}}`;
213
+ }
214
+ return "undefined";
215
+ };
7
216
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
217
  const generateMockFromType = (type, text) => {
9
218
  faker_1.faker.seed(123);
@@ -16,13 +225,13 @@ const generateMockFromType = (type, text) => {
16
225
  if (rightSideText.startsWith("[number, number] | [number, number][] | [number, number][][]")) {
17
226
  return "[[1, 2], [3, 4]]";
18
227
  }
19
- if (type.isStringLiteral()) {
228
+ if (Boolean(type.isStringLiteral())) {
20
229
  return type.compilerType.value; // Get the value of the string literal
21
230
  }
22
- if (type.isNumberLiteral()) {
231
+ if (Boolean(type.isNumberLiteral())) {
23
232
  return parseFloat(type.compilerType.value);
24
233
  }
25
- if (type.isBooleanLiteral()) {
234
+ if (Boolean(type.isBooleanLiteral())) {
26
235
  return type.compilerType.value === "true";
27
236
  }
28
237
  if (type.getText() === "string") {
@@ -34,14 +243,14 @@ const generateMockFromType = (type, text) => {
34
243
  if (type.getText() === "Date" || type.getText() === "DateTime") {
35
244
  return "2023-01-22T15:08:12.527Z";
36
245
  }
37
- if (type.isUnion()) {
38
- // We'll take the first type in the union for simplicity
39
- return generateMockFromType(type.getUnionTypes()[0], "");
246
+ if (Boolean(type.isUnion())) {
247
+ const nonNullish = type.getUnionTypes().filter((m) => !m.isNull() && !m.isUndefined());
248
+ return generateMockFromType(nonNullish[0] ?? type.getUnionTypes()[0], "");
40
249
  }
41
- if (type.isArray()) {
250
+ if (Boolean(type.isArray())) {
42
251
  return [generateMockFromType(type.getArrayElementTypeOrThrow(), "")];
43
252
  }
44
- if (type.isObject()) {
253
+ if (Boolean(type.isObject())) {
45
254
  const properties = type.getProperties();
46
255
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
256
  const obj = {};
@@ -49,7 +258,17 @@ const generateMockFromType = (type, text) => {
49
258
  const declaration = prop.getValueDeclarationOrThrow();
50
259
  const propType = declaration.getType();
51
260
  if (prop.getName() === "__typename") {
52
- obj[prop.getName()] = propType.getText().replace(/"/g, "");
261
+ let typenameType = propType;
262
+ if (Boolean(typenameType.isUnion())) {
263
+ const real = typenameType.getUnionTypes().filter((m) => !m.isNull() && !m.isUndefined());
264
+ typenameType = real[0] ?? typenameType;
265
+ }
266
+ obj[prop.getName()] = Boolean(typenameType.isStringLiteral())
267
+ ? typenameType.compilerType.value
268
+ : typenameType
269
+ .getText()
270
+ .replace(/"/g, "")
271
+ .replace(/ \| undefined/g, "");
53
272
  }
54
273
  else {
55
274
  obj[prop.getName()] = generateMockFromType(propType, declaration.getText());
@@ -68,6 +287,7 @@ const scrubMockFileContent = async (fileContent, nxRoot, isVerbose) => {
68
287
  compilerOptions: {
69
288
  noUnusedLocals: true,
70
289
  noUnusedParameters: true,
290
+ strictNullChecks: true,
71
291
  },
72
292
  });
73
293
  const generatedFile = project.createSourceFile("tmpMock.ts", fileContent);
@@ -89,10 +309,46 @@ const scrubMockFileContent = async (fileContent, nxRoot, isVerbose) => {
89
309
  }
90
310
  const mock = mockThese.map(mocker => {
91
311
  const postFix = (mocker.getName().includes("Query") ? "Query" : "Mutation").length;
92
- return `
93
-
94
- export const mockFor${mocker.getName()} = (variables: gql.${mocker.getName()}Variables, data?: DeepPartialNullable<gql.${mocker.getName()}> ) => {
95
- return queryFor(gql.${mocker.getName().substring(0, mocker.getName().length - postFix)}Document, variables,
312
+ const variablesTypeName = `${mocker.getName()}Variables`;
313
+ const variablesTypeAlias = generatedFile.getTypeAlias(variablesTypeName);
314
+ const docName = mocker.getName().substring(0, mocker.getName().length - postFix);
315
+ const typeDefaults = variablesTypeAlias
316
+ ? generateDefaultFromInputType(variablesTypeAlias.getType(), generatedFile)
317
+ : {};
318
+ const graphqlDefaults = extractGraphqlDefaultValues(generatedFile, `${docName}Document`);
319
+ const defaultVars = Object.keys(graphqlDefaults).length > 0 &&
320
+ typeDefaults &&
321
+ typeof typeDefaults === "object" &&
322
+ !Array.isArray(typeDefaults)
323
+ ? Object.assign({}, typeDefaults, graphqlDefaults)
324
+ : typeDefaults;
325
+ const requiredKeys = variablesTypeAlias ? getRequiredVariableKeys(variablesTypeAlias.getType(), generatedFile) : [];
326
+ let variablesParam;
327
+ if (requiredKeys.length === 0) {
328
+ variablesParam = `variables?: DeepPartialNullable<gql.${variablesTypeName}>`;
329
+ }
330
+ else {
331
+ const keysUnion = requiredKeys.map(k => `"${k}"`).join(" | ");
332
+ const requiredDeepPartial = `PickDeepPartialNullable<gql.${variablesTypeName}, ${keysUnion}>`;
333
+ const hasOptionalKeys = variablesTypeAlias
334
+ ? variablesTypeAlias.getType().getProperties().length > requiredKeys.length
335
+ : false;
336
+ const varType = hasOptionalKeys
337
+ ? `${requiredDeepPartial} & DeepPartialNullable<Omit<gql.${variablesTypeName}, ${keysUnion}>>`
338
+ : requiredDeepPartial;
339
+ variablesParam = `variables: ${varType}`;
340
+ }
341
+ const defaultVarsSource = serializeToSource(defaultVars, 4);
342
+ const isEmptyDefault = defaultVarsSource === "{}";
343
+ const defaultVarsRef = isEmptyDefault ? "{}" : `default${mocker.getName()}Variables`;
344
+ const constBlock = isEmptyDefault
345
+ ? ""
346
+ : `
347
+ const default${mocker.getName()}Variables: gql.${variablesTypeName} = ${defaultVarsSource};
348
+ `;
349
+ return `${constBlock}
350
+ export const mockFor${mocker.getName()} = (${variablesParam}, data?: DeepPartialNullable<gql.${mocker.getName()}> ) => {
351
+ return queryFor(gql.${docName}Document, mergeDeepVars(${defaultVarsRef}, variables),
96
352
  mergeDeep(
97
353
  ${JSON.stringify(generateMockFromType(mocker.getType(), ""), null, 4)}
98
354
  , data || {}) as gql.${mocker.getName()}
@@ -100,11 +356,10 @@ const scrubMockFileContent = async (fileContent, nxRoot, isVerbose) => {
100
356
  }`;
101
357
  });
102
358
  return `
103
- import { OperationVariables } from "@apollo/client";
104
359
  import { mergeDeep } from "@apollo/client/utilities";
105
- import { queryFor, DeepPartialNullable } from "@trackunit/react-core-contexts-test";
360
+ import { mergeDeepVars, queryFor, DeepPartialNullable, PickDeepPartialNullable } from "@trackunit/react-core-contexts-test";
106
361
  import * as gql from "./graphql";
107
-
362
+
108
363
  ${mock.join("\n")}
109
364
  `;
110
365
  };