index.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. /* eslint-disable no-restricted-syntax */
  3. /* eslint-disable no-use-before-define */
  4. import { createRequire } from 'node:module'
  5. import { faker } from '@faker-js/faker'
  6. import {
  7. Kind,
  8. type DocumentNode,
  9. OperationTypeNode,
  10. type OperationDefinitionNode,
  11. type VariableDefinitionNode,
  12. type TypeNode,
  13. type NamedTypeNode,
  14. } from 'graphql'
  15. import { uniqBy } from 'lodash-es'
  16. import {
  17. convertToGraphQLId,
  18. getIdFromGraphQLId,
  19. } from '#shared/graphql/utils.ts'
  20. import type { DeepPartial, DeepRequired } from '#shared/types/utils.ts'
  21. import getUuid from '#shared/utils/getUuid.ts'
  22. import logger from './logger.ts'
  23. import { generateGraphqlMockId, hasNodeParent, setNodeParent } from './utils.ts'
  24. const _require = createRequire(import.meta.url)
  25. const introspection = _require('../../../../graphql/graphql_introspection.json')
  26. export interface ResolversMeta {
  27. variables: Record<string, unknown>
  28. document: DocumentNode | undefined
  29. cached: boolean
  30. }
  31. type Resolver = (parent: any, defaults: any, meta: ResolversMeta) => any
  32. interface Resolvers {
  33. [key: string]: Resolver
  34. }
  35. const factoriesModules = import.meta.glob<Resolver>(
  36. ['../factories/**/*.ts', '!../factories/fixtures/*.ts'],
  37. {
  38. eager: true,
  39. import: 'default',
  40. },
  41. )
  42. const storedObjects = new Map<string, any>()
  43. export const getStoredMockedObject = <T>(
  44. type: string,
  45. id: number,
  46. ): DeepRequired<T> => {
  47. return storedObjects.get(convertToGraphQLId(type, id))
  48. }
  49. afterEach(() => {
  50. storedObjects.clear()
  51. })
  52. const factories: Resolvers = {}
  53. // eslint-disable-next-line guard-for-in
  54. for (const key in factoriesModules) {
  55. factories[
  56. key.replace(
  57. /\.\.\/factories\/(?:mutations|subscriptions|queries|types)\/(.*)\.ts$/,
  58. '$1',
  59. )
  60. ] = factoriesModules[key]
  61. }
  62. interface SchemaObjectType {
  63. kind: 'OBJECT'
  64. name: string
  65. fields: SchemaObjectField[]
  66. }
  67. interface SchemaEnumType {
  68. kind: 'ENUM'
  69. name: string
  70. enumValues: {
  71. name: string
  72. description: string
  73. }[]
  74. }
  75. interface SchemaObjectFieldType {
  76. kind: 'OBJECT' | 'ENUM' | 'LIST' | 'NON_NULL' | 'INPUT_OBJECT' | 'UNION'
  77. name: string
  78. ofType: null | SchemaObjectFieldType
  79. }
  80. interface SchemaObjectScalarFieldType {
  81. kind: 'SCALAR'
  82. name: string
  83. ofType: null
  84. }
  85. interface SchemaObjectField {
  86. name: string
  87. type: SchemaType
  88. args: SchemaObjectField[]
  89. isDeprecated?: boolean
  90. deprecatedReason?: string
  91. defaultValue?: unknown
  92. }
  93. interface SchemaScalarType {
  94. kind: 'SCALAR'
  95. name: string
  96. }
  97. interface SchemaUnionType {
  98. kind: 'UNION'
  99. possibleTypes: SchemaType[]
  100. name: string
  101. }
  102. interface SchemaInputObjectType {
  103. kind: 'INPUT_OBJECT'
  104. name: string
  105. fields: null
  106. inputFields: SchemaObjectField[]
  107. }
  108. type SchemaType =
  109. | SchemaObjectType
  110. | SchemaObjectFieldType
  111. | SchemaEnumType
  112. | SchemaScalarType
  113. | SchemaObjectScalarFieldType
  114. | SchemaUnionType
  115. | SchemaInputObjectType
  116. const schemaTypes = introspection.data.__schema.types as SchemaType[]
  117. const queriesTypes = {
  118. query: 'Queries',
  119. mutation: 'Mutations',
  120. subscription: 'Subscriptions',
  121. }
  122. const queries = schemaTypes.find(
  123. (type) => type.kind === 'OBJECT' && type.name === queriesTypes.query,
  124. ) as SchemaObjectType
  125. const mutations = schemaTypes.find(
  126. (type) => type.kind === 'OBJECT' && type.name === queriesTypes.mutation,
  127. ) as SchemaObjectType
  128. const subscriptions = schemaTypes.find(
  129. (type) => type.kind === 'OBJECT' && type.name === queriesTypes.subscription,
  130. ) as SchemaObjectType
  131. const schemas = {
  132. query: queries,
  133. mutation: mutations,
  134. subscription: subscriptions,
  135. }
  136. export const getOperationDefinition = (
  137. operation: OperationTypeNode,
  138. name: string,
  139. ) => {
  140. const { fields } = schemas[operation]
  141. return fields.find((field) => field.name === name)!
  142. }
  143. // there are some commonly named fields that backend uses
  144. // so we can assume what type they are
  145. const commonStringGenerators: Record<string, () => string> = {
  146. note: () => faker.lorem.sentence(),
  147. heading: () => faker.lorem.sentence(2),
  148. label: () => faker.lorem.sentence(2),
  149. value: () => faker.lorem.sentence(),
  150. uid: () => faker.string.uuid(),
  151. }
  152. // generate default scalar values
  153. const getScalarValue = (
  154. parent: any,
  155. fieldName: string,
  156. definition: SchemaScalarType,
  157. ): string | number | boolean | Record<string, unknown> => {
  158. switch (definition.name) {
  159. case 'Boolean':
  160. return faker.datatype.boolean()
  161. case 'Int':
  162. return faker.number.int({ min: 1, max: 1000 })
  163. case 'Float':
  164. return faker.number.float()
  165. case 'BinaryString':
  166. return faker.image.dataUri()
  167. case 'FormId':
  168. return getUuid()
  169. case 'ISO8601Date':
  170. return faker.date.recent().toISOString().substring(0, 10)
  171. case 'ISO8601DateTime':
  172. return faker.date.recent().toISOString()
  173. case 'ID':
  174. return generateGraphqlMockId(parent)
  175. case 'NonEmptyString':
  176. case 'String':
  177. return commonStringGenerators[fieldName]?.() || faker.lorem.word()
  178. case 'JSON':
  179. return {}
  180. case 'UriHttpString':
  181. return faker.internet.url()
  182. default:
  183. throw new Error(`not implemented for ${definition.name}`)
  184. }
  185. }
  186. const isList = (definitionType: SchemaType): boolean => {
  187. if (definitionType.kind === 'LIST') {
  188. return true
  189. }
  190. return 'ofType' in definitionType && definitionType.ofType
  191. ? isList(definitionType.ofType)
  192. : false
  193. }
  194. export const getFieldData = (definitionType: SchemaType): any => {
  195. if (
  196. definitionType.kind === 'SCALAR' ||
  197. definitionType.kind === 'OBJECT' ||
  198. definitionType.kind === 'UNION' ||
  199. definitionType.kind === 'INPUT_OBJECT'
  200. )
  201. return definitionType
  202. if (definitionType.kind === 'ENUM')
  203. return getEnumDefinition(definitionType.name)
  204. return definitionType.ofType ? getFieldData(definitionType.ofType) : null
  205. }
  206. const getFieldInformation = (definitionType: SchemaType) => {
  207. const list = isList(definitionType)
  208. const field = getFieldData(definitionType)
  209. if (!field) {
  210. console.dir(definitionType, { depth: null })
  211. throw new Error(`cannot find type definition for ${definitionType.name}`)
  212. }
  213. return {
  214. list,
  215. field,
  216. }
  217. }
  218. const getFromCache = (value: any, meta: ResolversMeta) => {
  219. if (!meta.cached) return undefined
  220. const potentialId =
  221. value.id ||
  222. (value.internalId
  223. ? convertToGraphQLId(value.__typename, value.internalId)
  224. : null)
  225. if (!potentialId) {
  226. // try to guess Id from variables
  227. const type = value.__typename
  228. const lowercaseType = type[0].toLowerCase() + type.slice(1)
  229. const potentialIdKey = `${lowercaseType}Id`
  230. const id = meta.variables[potentialIdKey] as string | undefined
  231. if (id) return storedObjects.get(id)
  232. const potentialInternalIdKey = `${lowercaseType}InternalId`
  233. const internalId = meta.variables[potentialInternalIdKey] as
  234. | number
  235. | undefined
  236. if (!internalId) return undefined
  237. const gqlId = convertToGraphQLId(type, internalId)
  238. return storedObjects.get(gqlId)
  239. }
  240. if (storedObjects.has(potentialId)) {
  241. return storedObjects.get(potentialId)
  242. }
  243. return undefined
  244. }
  245. // merges cache with custom defaults recursively by modifying the original object
  246. const deepMerge = (target: any, source: any): any => {
  247. // eslint-disable-next-line guard-for-in
  248. for (const key in source) {
  249. const value = source[key]
  250. if (typeof value === 'object' && value !== null) {
  251. if (Array.isArray(value)) {
  252. target[key] = value.map((v, index) => {
  253. if (
  254. typeof target[key]?.[index] === 'object' &&
  255. target[key]?.[index] !== null
  256. ) {
  257. return deepMerge(target[key]?.[index] || {}, v)
  258. }
  259. return v
  260. })
  261. } else {
  262. target[key] = deepMerge(target[key] || {}, value)
  263. }
  264. } else {
  265. target[key] = value
  266. }
  267. }
  268. return target
  269. }
  270. const populateObjectFromVariables = (value: any, meta: ResolversMeta) => {
  271. const type = value.__typename
  272. const lowercaseType = type[0].toLowerCase() + type.slice(1)
  273. const potentialIdKey = `${lowercaseType}Id`
  274. if (meta.variables[potentialIdKey]) {
  275. value.id ??= meta.variables[potentialIdKey]
  276. }
  277. const potentialInternalIdKey = `${lowercaseType}InternalId`
  278. if (meta.variables[potentialInternalIdKey]) {
  279. value.id ??= convertToGraphQLId(
  280. value.__typename,
  281. meta.variables[potentialInternalIdKey] as number,
  282. )
  283. value.internalId ??= meta.variables[potentialInternalIdKey]
  284. }
  285. }
  286. const getObjectDefinitionFromUnion = (fieldDefinition: any) => {
  287. if (fieldDefinition.kind === 'UNION') {
  288. const unionDefinition = getUnionDefinition(fieldDefinition.name)
  289. const randomObjectDefinition = faker.helpers.arrayElement(
  290. unionDefinition.possibleTypes,
  291. )
  292. return getObjectDefinition(randomObjectDefinition.name)
  293. }
  294. return fieldDefinition
  295. }
  296. const buildObjectFromInformation = (
  297. parent: any,
  298. fieldName: string,
  299. { list, field }: { list: boolean; field: any },
  300. defaults: any,
  301. meta: ResolversMeta,
  302. // eslint-disable-next-line sonarjs/cognitive-complexity
  303. ) => {
  304. if (field.kind === 'UNION' && !defaults) {
  305. const factory = factories[field.name as 'Avatar']
  306. if (factory) {
  307. defaults = list
  308. ? faker.helpers.multiple(() => factory(parent, undefined, meta), {
  309. count: { min: 1, max: 5 },
  310. })
  311. : factory(parent, undefined, meta)
  312. }
  313. }
  314. if (!list) {
  315. const typeDefinition = getObjectDefinitionFromUnion(field)
  316. return generateGqlValue(parent, fieldName, typeDefinition, defaults, meta)
  317. }
  318. if (defaults) {
  319. const isUnion = field.kind === 'UNION'
  320. const builtList = defaults.map((item: any) => {
  321. const actualFieldType =
  322. isUnion && item.__typename
  323. ? getObjectDefinition(item.__typename)
  324. : field
  325. return generateGqlValue(parent, fieldName, actualFieldType, item, meta)
  326. })
  327. if (typeof builtList[0] === 'object' && builtList[0]?.id) {
  328. return uniqBy(builtList, 'id') // if autocmocker generates duplicates, remove them
  329. }
  330. return builtList
  331. }
  332. const factory = factories[fieldName]
  333. if (factory) {
  334. logger.log(`[MOCKER] using factory for "${fieldName}"`)
  335. return factory(fieldName, defaults, meta)
  336. }
  337. const typeDefinition = getObjectDefinitionFromUnion(field)
  338. const builtList = faker.helpers.multiple(
  339. () => generateGqlValue(parent, fieldName, typeDefinition, undefined, meta),
  340. { count: { min: 1, max: 5 } },
  341. )
  342. if (typeof builtList[0] === 'object' && builtList[0]?.id) {
  343. return uniqBy(builtList, 'id') // if autocmocker generates duplicates, remove them
  344. }
  345. return builtList
  346. }
  347. // we always generate full object because it might be reused later
  348. // in another query with more parameters
  349. const generateObject = (
  350. parent: Record<string, any> | undefined,
  351. definition: SchemaObjectType,
  352. defaults: Record<string, any> | undefined,
  353. meta: ResolversMeta,
  354. // eslint-disable-next-line sonarjs/cognitive-complexity
  355. ): Record<string, any> | null => {
  356. logger.log(
  357. 'creating',
  358. definition.name,
  359. 'from',
  360. parent?.__typename || 'the root',
  361. `(${parent?.id ? getIdFromGraphQLId(parent?.id) : null})`,
  362. )
  363. if (defaults === null) return null
  364. const type = definition.name
  365. const value = defaults ? { ...defaults } : {}
  366. // Set the typename, if not already set by mocked value.
  367. value.__typename = value.__typename ?? type
  368. populateObjectFromVariables(value, meta)
  369. setNodeParent(value, parent)
  370. const cached = getFromCache(value, meta)
  371. if (cached !== undefined) {
  372. return defaults ? deepMerge(cached, defaults) : cached
  373. }
  374. const factory = factories[type as 'Avatar']
  375. if (factory) {
  376. const resolved = factory(parent, value, meta)
  377. // factory doesn't override custom defaults
  378. for (const key in resolved) {
  379. if (!(key in value)) {
  380. value[key] = resolved[key]
  381. } else if (
  382. value[key] &&
  383. typeof value[key] === 'object' &&
  384. resolved[key]
  385. ) {
  386. value[key] = deepMerge(resolved[key], value[key])
  387. }
  388. }
  389. }
  390. const factoryCached = getFromCache(value, meta)
  391. if (factoryCached !== undefined) {
  392. return factoryCached ? deepMerge(factoryCached, defaults) : factoryCached
  393. }
  394. if (value.id) {
  395. // I am not sure if this is a good change - it makes you think that ID is always numerical (even in prod) because of the tests
  396. // but it removes a lot of repetitive code - time will tell!
  397. if (typeof value.id !== 'string') {
  398. throw new Error(
  399. `id must be a string, got ${typeof value.id} inside ${type}`,
  400. )
  401. }
  402. // session has a unique base64 id
  403. if (type !== 'Session' && !value.id.startsWith('gid://zammad/')) {
  404. if (Number.isNaN(Number(value.id))) {
  405. throw new Error(
  406. `expected numerical or graphql id for ${type}, got ${value.id}`,
  407. )
  408. }
  409. const gqlId = convertToGraphQLId(type, value.id)
  410. logger.log(
  411. `received ${value.id} ID inside ${type}, rewriting to ${gqlId}`,
  412. )
  413. value.id = gqlId
  414. }
  415. storedObjects.set(value.id, value)
  416. }
  417. const needUpdateTotalCount =
  418. type.endsWith('Connection') && !('totalCount' in value)
  419. const buildField = (field: SchemaObjectField) => {
  420. const { name } = field
  421. // ignore null and undefined
  422. if (name in value && value[name] == null) {
  423. return
  424. }
  425. // if the object is already generated, keep it
  426. if (hasNodeParent(value[name])) {
  427. return
  428. }
  429. // by default, don't populate those fields since
  430. // first two can lead to recursions or inconsistent data
  431. // the "errors" should usually be "null" anyway
  432. // this is still possible to override with defaults
  433. if (
  434. !(name in value) &&
  435. (name === 'updatedBy' || name === 'createdBy' || name === 'errors')
  436. ) {
  437. value[name] = null
  438. return
  439. }
  440. // by default, all mutations are successful because errors are null
  441. if (
  442. field.type.kind === 'SCALAR' &&
  443. name === 'success' &&
  444. !(name in value)
  445. ) {
  446. value[name] = true
  447. return
  448. }
  449. value[name] = buildObjectFromInformation(
  450. value,
  451. name,
  452. getFieldInformation(field.type),
  453. value[name],
  454. meta,
  455. )
  456. if (meta.cached && name === 'id') {
  457. storedObjects.set(value.id, value)
  458. }
  459. }
  460. definition.fields!.forEach((field) => buildField(field))
  461. if (needUpdateTotalCount) {
  462. value.totalCount = value.edges.length
  463. }
  464. if (value.id && value.internalId) {
  465. value.internalId = getIdFromGraphQLId(value.id)
  466. }
  467. if (meta.cached && value.id) {
  468. storedObjects.set(value.id, value)
  469. }
  470. return value
  471. }
  472. export const getInputObjectDefinition = (name: string) => {
  473. const definition = schemaTypes.find(
  474. (type) => type.kind === 'INPUT_OBJECT' && type.name === name,
  475. ) as SchemaInputObjectType
  476. if (!definition) {
  477. throw new Error(`Input object definition not found for ${name}`)
  478. }
  479. return definition
  480. }
  481. export const getObjectDefinition = (name: string) => {
  482. const definition = schemaTypes.find(
  483. (type) => type.kind === 'OBJECT' && type.name === name,
  484. ) as SchemaObjectType
  485. if (!definition) {
  486. throw new Error(`Object definition not found for ${name}`)
  487. }
  488. return definition
  489. }
  490. const getUnionDefinition = (name: string) => {
  491. const definition = schemaTypes.find(
  492. (type) => type.kind === 'UNION' && type.name === name,
  493. ) as SchemaUnionType
  494. if (!definition) {
  495. throw new Error(`Union definition not found for ${name}`)
  496. }
  497. return definition
  498. }
  499. const getEnumDefinition = (name: string) => {
  500. const definition = schemaTypes.find(
  501. (type) => type.kind === 'ENUM' && type.name === name,
  502. ) as SchemaEnumType
  503. if (!definition) {
  504. throw new Error(`Enum definition not found for ${name}`)
  505. }
  506. return definition
  507. }
  508. const generateEnumValue = (definition: any): string => {
  509. return (faker.helpers.arrayElement(definition.enumValues) as { name: string })
  510. .name
  511. }
  512. const generateGqlValue = (
  513. parent: Record<string, any> | undefined,
  514. fieldName: string,
  515. typeDefinition: SchemaType,
  516. defaults: Record<string, any> | null | undefined,
  517. meta: ResolversMeta,
  518. ) => {
  519. if (defaults === null) return null
  520. if (typeDefinition.kind === 'OBJECT')
  521. return generateObject(
  522. parent,
  523. getObjectDefinition(typeDefinition.name),
  524. defaults,
  525. meta,
  526. )
  527. if (defaults !== undefined) return defaults
  528. if (typeDefinition.kind === 'ENUM') return generateEnumValue(typeDefinition)
  529. if (typeDefinition.kind === 'SCALAR')
  530. return getScalarValue(parent, fieldName, typeDefinition)
  531. logger.log(typeDefinition)
  532. throw new Error(`wrong definition for ${typeDefinition.name}`)
  533. }
  534. /**
  535. * Generates an object from a GraphQL type name.
  536. * You can provide a partial defaults object to make it more predictable.
  537. *
  538. * This function always generates a new object and never caches it.
  539. */
  540. export const generateObjectData = <T>(
  541. typename: string,
  542. defaults?: DeepPartial<T>,
  543. ): T => {
  544. return generateObject(undefined, getObjectDefinition(typename), defaults, {
  545. document: undefined,
  546. variables: {},
  547. cached: false,
  548. }) as T
  549. }
  550. const getJsTypeFromScalar = (
  551. scalar: string,
  552. ): 'string' | 'number' | 'boolean' | null => {
  553. switch (scalar) {
  554. case 'Boolean':
  555. return 'boolean'
  556. case 'Int':
  557. case 'Float':
  558. return 'number'
  559. case 'ID':
  560. case 'BinaryString':
  561. case 'NonEmptyString':
  562. case 'FormId':
  563. case 'String':
  564. case 'ISO8601Date':
  565. case 'ISO8601DateTime':
  566. return 'string'
  567. case 'JSON':
  568. default:
  569. return null
  570. }
  571. }
  572. const createVariablesError = (
  573. definition: OperationDefinitionNode,
  574. message: string,
  575. ) => {
  576. return new Error(
  577. `(Variables error for ${definition.operation} ${definition.name?.value}) ${message}`,
  578. )
  579. }
  580. const validateSchemaValue = (
  581. definition: OperationDefinitionNode,
  582. data: SchemaObjectField,
  583. value: any,
  584. // eslint-disable-next-line sonarjs/cognitive-complexity
  585. ) => {
  586. const required = data.type.kind === 'NON_NULL'
  587. const { field, list } = getFieldInformation(data.type)
  588. if (value == null) {
  589. if (required && data.defaultValue == null) {
  590. throw createVariablesError(
  591. definition,
  592. `non-nullable field "${data.name}" is not defined`,
  593. )
  594. }
  595. return
  596. }
  597. if (list) {
  598. if (!Array.isArray(value)) {
  599. throw createVariablesError(
  600. definition,
  601. `expected array for "${data.name}", got ${typeof value}`,
  602. )
  603. }
  604. for (const item of value) {
  605. validateSchemaValue(definition, { ...data, type: field }, item)
  606. }
  607. return
  608. }
  609. if (field.kind === 'SCALAR') {
  610. const type = getJsTypeFromScalar(field.name)
  611. // eslint-disable-next-line valid-typeof
  612. if (type && typeof value !== type) {
  613. throw createVariablesError(
  614. definition,
  615. `expected ${type} for "${data.name}", got ${typeof value}`,
  616. )
  617. }
  618. }
  619. if (field.kind === 'ENUM') {
  620. const enumDefinition = getEnumDefinition(field.name)
  621. const enumValues = enumDefinition.enumValues.map(
  622. (enumValue) => enumValue.name,
  623. )
  624. if (!enumValues.includes(value)) {
  625. throw createVariablesError(
  626. definition,
  627. `${data.name} should be one of "${enumValues.join('", "')}", but instead got "${value}"`,
  628. )
  629. }
  630. }
  631. if (field.kind === 'INPUT_OBJECT') {
  632. if (typeof value !== 'object' || value === null) {
  633. throw createVariablesError(
  634. definition,
  635. `expected object for "${data.name}", got ${typeof value}`,
  636. )
  637. }
  638. const object = getInputObjectDefinition(field.name)
  639. const fields = new Set(object.inputFields.map((f) => f.name))
  640. for (const key in value) {
  641. if (!fields.has(key)) {
  642. throw createVariablesError(
  643. definition,
  644. `field "${key}" is not defined on ${field.name}`,
  645. )
  646. }
  647. }
  648. for (const field of object.inputFields) {
  649. const inputValue = value[field.name]
  650. validateSchemaValue(definition, field, inputValue)
  651. }
  652. }
  653. }
  654. const isVariableDefinitionList = (definition: VariableDefinitionNode) => {
  655. if (definition.type.kind === Kind.LIST_TYPE) return true
  656. if (definition.type.kind === Kind.NON_NULL_TYPE) {
  657. return definition.type.type.kind === Kind.LIST_TYPE
  658. }
  659. return false
  660. }
  661. const getVariableDefinitionField = (definition: TypeNode): NamedTypeNode => {
  662. if (definition.kind === Kind.NAMED_TYPE) return definition
  663. return getVariableDefinitionField(definition.type)
  664. }
  665. const buildFieldDefinitionFromVriablesDefinition = ({
  666. list,
  667. required,
  668. field,
  669. }: {
  670. list: boolean
  671. required: boolean
  672. field: any
  673. }) => {
  674. if (list && required) {
  675. return {
  676. kind: 'NON_NULL',
  677. ofType: {
  678. kind: 'LIST',
  679. ofType: field,
  680. },
  681. }
  682. }
  683. if (required) {
  684. return {
  685. kind: 'NON_NULL',
  686. ofType: field,
  687. }
  688. }
  689. if (list) {
  690. return {
  691. kind: 'LIST',
  692. ofType: field,
  693. }
  694. }
  695. return field
  696. }
  697. const validateQueryDefinitionVariables = (
  698. definition: OperationDefinitionNode,
  699. variableDefinition: VariableDefinitionNode,
  700. variables: Record<string, any>,
  701. ) => {
  702. const required = variableDefinition.type.kind === Kind.NON_NULL_TYPE
  703. const list = isVariableDefinitionList(variableDefinition)
  704. const field = getVariableDefinitionField(variableDefinition.type)
  705. const fieldDefinitions = schemaTypes.filter(
  706. (type) => type.name === field.name.value,
  707. )
  708. if (fieldDefinitions.length === 0) {
  709. throw createVariablesError(
  710. definition,
  711. `Cannot find definition for "${field.name.value}"`,
  712. )
  713. }
  714. if (fieldDefinitions.length > 1) {
  715. throw createVariablesError(
  716. definition,
  717. `Multiple definitions for "${field.name.value}": ${fieldDefinitions.map((type) => type.kind).join(', ')}`,
  718. )
  719. }
  720. const name = variableDefinition.variable.name.value
  721. const defaultValue = (() => {
  722. const { defaultValue } = variableDefinition
  723. if (typeof defaultValue === 'undefined') return undefined
  724. if ('value' in defaultValue) return defaultValue.value
  725. if (defaultValue.kind === Kind.LIST) return []
  726. if (defaultValue.kind === Kind.NULL) return null
  727. // we don't care for values inside the object
  728. return {}
  729. })()
  730. validateSchemaValue(
  731. definition,
  732. {
  733. name,
  734. type: buildFieldDefinitionFromVriablesDefinition({
  735. required,
  736. list,
  737. field: fieldDefinitions[0],
  738. }),
  739. args: [],
  740. defaultValue,
  741. },
  742. variables[name],
  743. )
  744. }
  745. export const validateOperationVariables = (
  746. definition: OperationDefinitionNode,
  747. variables: Record<string, any>,
  748. ) => {
  749. const name = definition.name?.value
  750. if (!name || !definition.variableDefinitions) return
  751. const variablesNames = definition.variableDefinitions.map(
  752. (variableDefinition) => variableDefinition.variable.name.value,
  753. )
  754. for (const variable in variables) {
  755. if (!variablesNames.includes(variable)) {
  756. throw createVariablesError(
  757. definition,
  758. `field "${variable}" is not defined on ${definition.operation} ${name}`,
  759. )
  760. }
  761. }
  762. definition.variableDefinitions.forEach((variableDefinition) => {
  763. validateQueryDefinitionVariables(definition, variableDefinition, variables)
  764. })
  765. }
  766. export const mockOperation = (
  767. document: DocumentNode,
  768. variables: Record<string, unknown>,
  769. defaults?: Record<string, any>,
  770. // eslint-disable-next-line sonarjs/cognitive-complexity
  771. ): Record<string, any> => {
  772. const definition = document.definitions[0]
  773. if (definition.kind !== Kind.OPERATION_DEFINITION) {
  774. throw new Error(`${(definition as any).name} is not an operation`)
  775. }
  776. const { operation, name, selectionSet } = definition
  777. const operationName = name!.value!
  778. let operationType = getOperationDefinition(operation, operationName)
  779. // In case the operation cannot be inferred from the operation name, switch to selection name instead.
  780. // E.g. `currentUserUpdates` vs `userUpdates`
  781. if (!operationType && selectionSet.selections.length === 1) {
  782. const selection = selectionSet.selections[0]
  783. if (selection.kind !== Kind.FIELD) {
  784. throw new Error(
  785. `unsupported selection kind ${selectionSet.selections[0].kind}`,
  786. )
  787. }
  788. operationType = getOperationDefinition(operation, selection.name.value)
  789. if (!operationType)
  790. throw new Error(
  791. `unsupported operation named ${operationName} or ${selection.name.value}`,
  792. )
  793. }
  794. const query: any = { __typename: queriesTypes[operation] }
  795. const rootName = operationType?.name || operationName
  796. logger.log(`[MOCKER] mocking "${rootName}" ${operation}`)
  797. if (selectionSet.selections.length === 1) {
  798. const information = getFieldInformation(operationType.type)
  799. const selection = selectionSet.selections[0]
  800. if (selection.kind !== Kind.FIELD) {
  801. throw new Error(
  802. `unsupported selection kind ${selectionSet.selections[0].kind}`,
  803. )
  804. }
  805. if (selection.name.value !== rootName) {
  806. throw new Error(
  807. `unsupported selection name ${selection.name.value} (${operation} is ${operationType.name})`,
  808. )
  809. }
  810. query[rootName] = buildObjectFromInformation(
  811. query,
  812. rootName,
  813. information,
  814. defaults?.[rootName],
  815. {
  816. document,
  817. variables,
  818. cached: true,
  819. },
  820. )
  821. } else {
  822. selectionSet.selections.forEach((selection) => {
  823. if (selection.kind !== Kind.FIELD) {
  824. throw new Error(`unsupported selection kind ${selection.kind}`)
  825. }
  826. const operationType = getOperationDefinition(
  827. operation,
  828. selection.name.value,
  829. )
  830. if (!operationType) {
  831. throw new Error(`unsupported operation named ${operationName}`)
  832. }
  833. const fieldName = selection.alias?.value || selection.name.value
  834. query[fieldName] = buildObjectFromInformation(
  835. query,
  836. rootName,
  837. getFieldInformation(operationType.type),
  838. defaults?.[fieldName] ?? defaults?.[rootName],
  839. {
  840. document,
  841. variables,
  842. cached: true,
  843. },
  844. )
  845. })
  846. }
  847. return query
  848. }