index.ts 24 KB

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