index.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  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 { 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>('../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. case 'UriHttpString':
  174. return faker.internet.url()
  175. default:
  176. throw new Error(`not implemented for ${definition.name}`)
  177. }
  178. }
  179. const isList = (definitionType: SchemaType): boolean => {
  180. if (definitionType.kind === 'LIST') {
  181. return true
  182. }
  183. return 'ofType' in definitionType && definitionType.ofType
  184. ? isList(definitionType.ofType)
  185. : false
  186. }
  187. export const getFieldData = (definitionType: SchemaType): any => {
  188. if (
  189. definitionType.kind === 'SCALAR' ||
  190. definitionType.kind === 'OBJECT' ||
  191. definitionType.kind === 'UNION' ||
  192. definitionType.kind === 'INPUT_OBJECT'
  193. )
  194. return definitionType
  195. if (definitionType.kind === 'ENUM')
  196. return getEnumDefinition(definitionType.name)
  197. return definitionType.ofType ? getFieldData(definitionType.ofType) : null
  198. }
  199. const getFieldInformation = (definitionType: SchemaType) => {
  200. const list = isList(definitionType)
  201. const field = getFieldData(definitionType)
  202. if (!field) {
  203. console.dir(definitionType, { depth: null })
  204. throw new Error(`cannot find type definition for ${definitionType.name}`)
  205. }
  206. return {
  207. list,
  208. field,
  209. }
  210. }
  211. const getFromCache = (value: any, meta: ResolversMeta) => {
  212. if (!meta.cached) return undefined
  213. const potentialId =
  214. value.id ||
  215. (value.internalId
  216. ? convertToGraphQLId(value.__typename, value.internalId)
  217. : null)
  218. if (!potentialId) {
  219. // try to guess Id from variables
  220. const type = value.__typename
  221. const lowercaseType = type[0].toLowerCase() + type.slice(1)
  222. const potentialIdKey = `${lowercaseType}Id`
  223. const id = meta.variables[potentialIdKey] as string | undefined
  224. if (id) return storedObjects.get(id)
  225. const potentialInternalIdKey = `${lowercaseType}InternalId`
  226. const internalId = meta.variables[potentialInternalIdKey] as
  227. | number
  228. | undefined
  229. if (!internalId) return undefined
  230. const gqlId = convertToGraphQLId(type, internalId)
  231. return storedObjects.get(gqlId)
  232. }
  233. if (storedObjects.has(potentialId)) {
  234. return storedObjects.get(potentialId)
  235. }
  236. return undefined
  237. }
  238. // merges cache with custom defaults recursively by modifying the original object
  239. const deepMerge = (target: any, source: any): any => {
  240. // eslint-disable-next-line guard-for-in
  241. for (const key in source) {
  242. const value = source[key]
  243. if (typeof value === 'object' && value !== null) {
  244. if (Array.isArray(value)) {
  245. target[key] = value.map((v, index) => {
  246. if (
  247. typeof target[key]?.[index] === 'object' &&
  248. target[key]?.[index] !== null
  249. ) {
  250. return deepMerge(target[key]?.[index] || {}, v)
  251. }
  252. return v
  253. })
  254. } else {
  255. target[key] = deepMerge(target[key] || {}, value)
  256. }
  257. } else {
  258. target[key] = value
  259. }
  260. }
  261. return target
  262. }
  263. const populateObjectFromVariables = (value: any, meta: ResolversMeta) => {
  264. const type = value.__typename
  265. const lowercaseType = type[0].toLowerCase() + type.slice(1)
  266. const potentialIdKey = `${lowercaseType}Id`
  267. if (meta.variables[potentialIdKey]) {
  268. value.id ??= meta.variables[potentialIdKey]
  269. }
  270. const potentialInternalIdKey = `${lowercaseType}InternalId`
  271. if (meta.variables[potentialInternalIdKey]) {
  272. value.id ??= convertToGraphQLId(
  273. value.__typename,
  274. meta.variables[potentialInternalIdKey] as number,
  275. )
  276. value.internalId ??= meta.variables[potentialInternalIdKey]
  277. }
  278. }
  279. const getObjectDefinitionFromUnion = (fieldDefinition: any) => {
  280. if (fieldDefinition.kind === 'UNION') {
  281. const unionDefinition = getUnionDefinition(fieldDefinition.name)
  282. const randomObjectDefinition = faker.helpers.arrayElement(
  283. unionDefinition.possibleTypes,
  284. )
  285. return getObjectDefinition(randomObjectDefinition.name)
  286. }
  287. return fieldDefinition
  288. }
  289. const buildObjectFromInformation = (
  290. parent: any,
  291. fieldName: string,
  292. { list, field }: { list: boolean; field: any },
  293. defaults: any,
  294. meta: ResolversMeta,
  295. // eslint-disable-next-line sonarjs/cognitive-complexity
  296. ) => {
  297. if (field.kind === 'UNION' && !defaults) {
  298. const factory = factories[field.name as 'Avatar']
  299. if (factory) {
  300. defaults = list
  301. ? faker.helpers.multiple(() => factory(parent, undefined, meta), {
  302. count: { min: 1, max: 5 },
  303. })
  304. : factory(parent, undefined, meta)
  305. }
  306. }
  307. if (!list) {
  308. const typeDefinition = getObjectDefinitionFromUnion(field)
  309. return generateGqlValue(parent, fieldName, typeDefinition, defaults, meta)
  310. }
  311. if (defaults) {
  312. const isUnion = field.kind === 'UNION'
  313. const builtList = defaults.map((item: any) => {
  314. const actualFieldType =
  315. isUnion && item.__typename
  316. ? getObjectDefinition(item.__typename)
  317. : field
  318. return generateGqlValue(parent, fieldName, actualFieldType, item, meta)
  319. })
  320. if (typeof builtList[0] === 'object' && builtList[0]?.id) {
  321. return uniqBy(builtList, 'id') // if autocmocker generates duplicates, remove them
  322. }
  323. return builtList
  324. }
  325. const typeDefinition = getObjectDefinitionFromUnion(field)
  326. const builtList = faker.helpers.multiple(
  327. () => generateGqlValue(parent, fieldName, typeDefinition, undefined, meta),
  328. { count: { min: 1, max: 5 } },
  329. )
  330. if (typeof builtList[0] === 'object' && builtList[0]?.id) {
  331. return uniqBy(builtList, 'id') // if autocmocker generates duplicates, remove them
  332. }
  333. return builtList
  334. }
  335. // we always generate full object because it might be reused later
  336. // in another query with more parameters
  337. const generateObject = (
  338. parent: Record<string, any> | undefined,
  339. definition: SchemaObjectType,
  340. defaults: Record<string, any> | undefined,
  341. meta: ResolversMeta,
  342. // eslint-disable-next-line sonarjs/cognitive-complexity
  343. ): Record<string, any> | null => {
  344. logger.log(
  345. 'creating',
  346. definition.name,
  347. 'from',
  348. parent?.__typename || 'the root',
  349. `(${parent?.id ? getIdFromGraphQLId(parent?.id) : null})`,
  350. )
  351. if (defaults === null) return null
  352. const type = definition.name
  353. const value = defaults ? { ...defaults } : {}
  354. // Set the typename, if not already set by mocked value.
  355. value.__typename = value.__typename ?? type
  356. populateObjectFromVariables(value, meta)
  357. setNodeParent(value, parent)
  358. const cached = getFromCache(value, meta)
  359. if (cached !== undefined) {
  360. return defaults ? deepMerge(cached, defaults) : cached
  361. }
  362. const factory = factories[type as 'Avatar']
  363. if (factory) {
  364. const resolved = factory(parent, value, meta)
  365. // factory doesn't override custom defaults
  366. for (const key in resolved) {
  367. if (!(key in value)) {
  368. value[key] = resolved[key]
  369. } else if (
  370. value[key] &&
  371. typeof value[key] === 'object' &&
  372. resolved[key]
  373. ) {
  374. value[key] = deepMerge(resolved[key], value[key])
  375. }
  376. }
  377. }
  378. const factoryCached = getFromCache(value, meta)
  379. if (factoryCached !== undefined) {
  380. return factoryCached ? deepMerge(factoryCached, defaults) : factoryCached
  381. }
  382. if (value.id) {
  383. // 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
  384. // but it removes a lot of repetitive code - time will tell!
  385. if (typeof value.id !== 'string') {
  386. throw new Error(
  387. `id must be a string, got ${typeof value.id} inside ${type}`,
  388. )
  389. }
  390. // session has a unique base64 id
  391. if (type !== 'Session' && !value.id.startsWith('gid://zammad/')) {
  392. if (Number.isNaN(Number(value.id))) {
  393. throw new Error(
  394. `expected numerical or graphql id for ${type}, got ${value.id}`,
  395. )
  396. }
  397. const gqlId = convertToGraphQLId(type, value.id)
  398. logger.log(
  399. `received ${value.id} ID inside ${type}, rewriting to ${gqlId}`,
  400. )
  401. value.id = gqlId
  402. }
  403. storedObjects.set(value.id, value)
  404. }
  405. const needUpdateTotalCount =
  406. type.endsWith('Connection') && !('totalCount' in value)
  407. const buildField = (field: SchemaObjectField) => {
  408. const { name } = field
  409. // ignore null and undefined
  410. if (name in value && value[name] == null) {
  411. return
  412. }
  413. // if the object is already generated, keep it
  414. if (hasNodeParent(value[name])) {
  415. return
  416. }
  417. // by default, don't populate those fields since
  418. // first two can lead to recursions or inconsistent data
  419. // the "errors" should usually be "null" anyway
  420. // this is still possible to override with defaults
  421. if (
  422. !(name in value) &&
  423. (name === 'updatedBy' || name === 'createdBy' || name === 'errors')
  424. ) {
  425. value[name] = null
  426. return
  427. }
  428. // by default, all mutations are successful because errors are null
  429. if (
  430. field.type.kind === 'SCALAR' &&
  431. name === 'success' &&
  432. !(name in value)
  433. ) {
  434. value[name] = true
  435. return
  436. }
  437. value[name] = buildObjectFromInformation(
  438. value,
  439. name,
  440. getFieldInformation(field.type),
  441. value[name],
  442. meta,
  443. )
  444. if (meta.cached && name === 'id') {
  445. storedObjects.set(value.id, value)
  446. }
  447. }
  448. definition.fields!.forEach((field) => buildField(field))
  449. if (needUpdateTotalCount) {
  450. value.totalCount = value.edges.length
  451. }
  452. if (value.id && value.internalId) {
  453. value.internalId = getIdFromGraphQLId(value.id)
  454. }
  455. if (meta.cached && value.id) {
  456. storedObjects.set(value.id, value)
  457. }
  458. return value
  459. }
  460. export const getInputObjectDefinition = (name: string) => {
  461. const definition = schemaTypes.find(
  462. (type) => type.kind === 'INPUT_OBJECT' && type.name === name,
  463. ) as SchemaInputObjectType
  464. if (!definition) {
  465. throw new Error(`Input object definition not found for ${name}`)
  466. }
  467. return definition
  468. }
  469. export const getObjectDefinition = (name: string) => {
  470. const definition = schemaTypes.find(
  471. (type) => type.kind === 'OBJECT' && type.name === name,
  472. ) as SchemaObjectType
  473. if (!definition) {
  474. throw new Error(`Object definition not found for ${name}`)
  475. }
  476. return definition
  477. }
  478. const getUnionDefinition = (name: string) => {
  479. const definition = schemaTypes.find(
  480. (type) => type.kind === 'UNION' && type.name === name,
  481. ) as SchemaUnionType
  482. if (!definition) {
  483. throw new Error(`Union definition not found for ${name}`)
  484. }
  485. return definition
  486. }
  487. const getEnumDefinition = (name: string) => {
  488. const definition = schemaTypes.find(
  489. (type) => type.kind === 'ENUM' && type.name === name,
  490. ) as SchemaEnumType
  491. if (!definition) {
  492. throw new Error(`Enum definition not found for ${name}`)
  493. }
  494. return definition
  495. }
  496. const generateEnumValue = (definition: any): string => {
  497. return (faker.helpers.arrayElement(definition.enumValues) as { name: string })
  498. .name
  499. }
  500. const generateGqlValue = (
  501. parent: Record<string, any> | undefined,
  502. fieldName: string,
  503. typeDefinition: SchemaType,
  504. defaults: Record<string, any> | null | undefined,
  505. meta: ResolversMeta,
  506. ) => {
  507. if (defaults === null) return null
  508. if (typeDefinition.kind === 'OBJECT')
  509. return generateObject(
  510. parent,
  511. getObjectDefinition(typeDefinition.name),
  512. defaults,
  513. meta,
  514. )
  515. if (defaults !== undefined) return defaults
  516. if (typeDefinition.kind === 'ENUM') return generateEnumValue(typeDefinition)
  517. if (typeDefinition.kind === 'SCALAR')
  518. return getScalarValue(parent, fieldName, typeDefinition)
  519. logger.log(typeDefinition)
  520. throw new Error(`wrong definition for ${typeDefinition.name}`)
  521. }
  522. /**
  523. * Generates an object from a GraphQL type name.
  524. * You can provide a partial defaults object to make it more predictable.
  525. *
  526. * This function always generates a new object and never caches it.
  527. */
  528. export const generateObjectData = <T>(
  529. typename: string,
  530. defaults?: DeepPartial<T>,
  531. ): T => {
  532. return generateObject(undefined, getObjectDefinition(typename), defaults, {
  533. document: undefined,
  534. variables: {},
  535. cached: false,
  536. }) as T
  537. }
  538. const getJsTypeFromScalar = (
  539. scalar: string,
  540. ): 'string' | 'number' | 'boolean' | null => {
  541. switch (scalar) {
  542. case 'Boolean':
  543. return 'boolean'
  544. case 'Int':
  545. case 'Float':
  546. return 'number'
  547. case 'ID':
  548. case 'BinaryString':
  549. case 'NonEmptyString':
  550. case 'FormId':
  551. case 'String':
  552. case 'ISO8601Date':
  553. case 'ISO8601DateTime':
  554. return 'string'
  555. case 'JSON':
  556. default:
  557. return null
  558. }
  559. }
  560. const createVariablesError = (
  561. definition: OperationDefinitionNode,
  562. message: string,
  563. ) => {
  564. return new Error(
  565. `(Variables error for ${definition.operation} ${definition.name?.value}) ${message}`,
  566. )
  567. }
  568. const validateSchemaValue = (
  569. definition: OperationDefinitionNode,
  570. data: SchemaObjectField,
  571. value: any,
  572. // eslint-disable-next-line sonarjs/cognitive-complexity
  573. ) => {
  574. const required = data.type.kind === 'NON_NULL'
  575. const { field, list } = getFieldInformation(data.type)
  576. if (value == null) {
  577. if (required && data.defaultValue == null) {
  578. throw createVariablesError(
  579. definition,
  580. `non-nullable field "${data.name}" is not defined`,
  581. )
  582. }
  583. return
  584. }
  585. if (list) {
  586. if (!Array.isArray(value)) {
  587. throw createVariablesError(
  588. definition,
  589. `expected array for "${data.name}", got ${typeof value}`,
  590. )
  591. }
  592. for (const item of value) {
  593. validateSchemaValue(definition, { ...data, type: field }, item)
  594. }
  595. return
  596. }
  597. if (field.kind === 'SCALAR') {
  598. const type = getJsTypeFromScalar(field.name)
  599. // eslint-disable-next-line valid-typeof
  600. if (type && typeof value !== type) {
  601. throw createVariablesError(
  602. definition,
  603. `expected ${type} for "${data.name}", got ${typeof value}`,
  604. )
  605. }
  606. }
  607. if (field.kind === 'ENUM') {
  608. const enumDefinition = getEnumDefinition(field.name)
  609. const enumValues = enumDefinition.enumValues.map(
  610. (enumValue) => enumValue.name,
  611. )
  612. if (!enumValues.includes(value)) {
  613. throw createVariablesError(
  614. definition,
  615. `${data.name} should be one of "${enumValues.join('", "')}", but instead got "${value}"`,
  616. )
  617. }
  618. }
  619. if (field.kind === 'INPUT_OBJECT') {
  620. if (typeof value !== 'object' || value === null) {
  621. throw createVariablesError(
  622. definition,
  623. `expected object for "${data.name}", got ${typeof value}`,
  624. )
  625. }
  626. const object = getInputObjectDefinition(field.name)
  627. const fields = new Set(object.inputFields.map((f) => f.name))
  628. for (const key in value) {
  629. if (!fields.has(key)) {
  630. throw createVariablesError(
  631. definition,
  632. `field "${key}" is not defined on ${field.name}`,
  633. )
  634. }
  635. }
  636. for (const field of object.inputFields) {
  637. const inputValue = value[field.name]
  638. validateSchemaValue(definition, field, inputValue)
  639. }
  640. }
  641. }
  642. const isVariableDefinitionList = (definition: VariableDefinitionNode) => {
  643. if (definition.type.kind === Kind.LIST_TYPE) return true
  644. if (definition.type.kind === Kind.NON_NULL_TYPE) {
  645. return definition.type.type.kind === Kind.LIST_TYPE
  646. }
  647. return false
  648. }
  649. const getVariableDefinitionField = (definition: TypeNode): NamedTypeNode => {
  650. if (definition.kind === Kind.NAMED_TYPE) return definition
  651. return getVariableDefinitionField(definition.type)
  652. }
  653. const buildFieldDefinitionFromVriablesDefinition = ({
  654. list,
  655. required,
  656. field,
  657. }: {
  658. list: boolean
  659. required: boolean
  660. field: any
  661. }) => {
  662. if (list && required) {
  663. return {
  664. kind: 'NON_NULL',
  665. ofType: {
  666. kind: 'LIST',
  667. ofType: field,
  668. },
  669. }
  670. }
  671. if (required) {
  672. return {
  673. kind: 'NON_NULL',
  674. ofType: field,
  675. }
  676. }
  677. if (list) {
  678. return {
  679. kind: 'LIST',
  680. ofType: field,
  681. }
  682. }
  683. return field
  684. }
  685. const validateQueryDefinitionVariables = (
  686. definition: OperationDefinitionNode,
  687. variableDefinition: VariableDefinitionNode,
  688. variables: Record<string, any>,
  689. ) => {
  690. const required = variableDefinition.type.kind === Kind.NON_NULL_TYPE
  691. const list = isVariableDefinitionList(variableDefinition)
  692. const field = getVariableDefinitionField(variableDefinition.type)
  693. const fieldDefinitions = schemaTypes.filter(
  694. (type) => type.name === field.name.value,
  695. )
  696. if (fieldDefinitions.length === 0) {
  697. throw createVariablesError(
  698. definition,
  699. `Cannot find definition for "${field.name.value}"`,
  700. )
  701. }
  702. if (fieldDefinitions.length > 1) {
  703. throw createVariablesError(
  704. definition,
  705. `Multiple definitions for "${field.name.value}": ${fieldDefinitions.map((type) => type.kind).join(', ')}`,
  706. )
  707. }
  708. const name = variableDefinition.variable.name.value
  709. const defaultValue = (() => {
  710. const { defaultValue } = variableDefinition
  711. if (typeof defaultValue === 'undefined') return undefined
  712. if ('value' in defaultValue) return defaultValue.value
  713. if (defaultValue.kind === Kind.LIST) return []
  714. if (defaultValue.kind === Kind.NULL) return null
  715. // we don't care for values inside the object
  716. return {}
  717. })()
  718. validateSchemaValue(
  719. definition,
  720. {
  721. name,
  722. type: buildFieldDefinitionFromVriablesDefinition({
  723. required,
  724. list,
  725. field: fieldDefinitions[0],
  726. }),
  727. args: [],
  728. defaultValue,
  729. },
  730. variables[name],
  731. )
  732. }
  733. export const validateOperationVariables = (
  734. definition: OperationDefinitionNode,
  735. variables: Record<string, any>,
  736. ) => {
  737. const name = definition.name?.value
  738. if (!name || !definition.variableDefinitions) return
  739. const variablesNames = definition.variableDefinitions.map(
  740. (variableDefinition) => variableDefinition.variable.name.value,
  741. )
  742. for (const variable in variables) {
  743. if (!variablesNames.includes(variable)) {
  744. throw createVariablesError(
  745. definition,
  746. `field "${variable}" is not defined on ${definition.operation} ${name}`,
  747. )
  748. }
  749. }
  750. definition.variableDefinitions.forEach((variableDefinition) => {
  751. validateQueryDefinitionVariables(definition, variableDefinition, variables)
  752. })
  753. }
  754. export const mockOperation = (
  755. document: DocumentNode,
  756. variables: Record<string, unknown>,
  757. defaults?: Record<string, any>,
  758. // eslint-disable-next-line sonarjs/cognitive-complexity
  759. ): Record<string, any> => {
  760. const definition = document.definitions[0]
  761. if (definition.kind !== Kind.OPERATION_DEFINITION) {
  762. throw new Error(`${(definition as any).name} is not an operation`)
  763. }
  764. const { operation, name, selectionSet } = definition
  765. const operationName = name!.value!
  766. let operationType = getOperationDefinition(operation, operationName)
  767. // In case the operation cannot be inferred from the operation name, switch to selection name instead.
  768. // E.g. `currentUserUpdates` vs `userUpdates`
  769. if (!operationType && selectionSet.selections.length === 1) {
  770. const selection = selectionSet.selections[0]
  771. if (selection.kind !== Kind.FIELD) {
  772. throw new Error(
  773. `unsupported selection kind ${selectionSet.selections[0].kind}`,
  774. )
  775. }
  776. operationType = getOperationDefinition(operation, selection.name.value)
  777. if (!operationType)
  778. throw new Error(
  779. `unsupported operation named ${operationName} or ${selection.name.value}`,
  780. )
  781. } else if (!operationType) {
  782. throw new Error(`unsupported operation named ${operationName}`)
  783. }
  784. const query: any = { __typename: queriesTypes[operation] }
  785. const rootName = operationType.name
  786. logger.log(`[MOCKER] mocking "${rootName}" ${operation}`)
  787. const information = getFieldInformation(operationType.type)
  788. if (selectionSet.selections.length === 1) {
  789. const selection = selectionSet.selections[0]
  790. if (selection.kind !== Kind.FIELD) {
  791. throw new Error(
  792. `unsupported selection kind ${selectionSet.selections[0].kind}`,
  793. )
  794. }
  795. if (selection.name.value !== rootName) {
  796. throw new Error(
  797. `unsupported selection name ${selection.name.value} (${operation} is ${operationType.name})`,
  798. )
  799. }
  800. query[rootName] = buildObjectFromInformation(
  801. query,
  802. rootName,
  803. information,
  804. defaults?.[rootName],
  805. {
  806. document,
  807. variables,
  808. cached: true,
  809. },
  810. )
  811. } else {
  812. selectionSet.selections.forEach((selection) => {
  813. if (selection.kind !== Kind.FIELD) {
  814. throw new Error(`unsupported selection kind ${selection.kind}`)
  815. }
  816. const operationType = getOperationDefinition(operation, operationName)
  817. const fieldName = selection.alias?.value || selection.name.value
  818. query[fieldName] = buildObjectFromInformation(
  819. query,
  820. rootName,
  821. getFieldInformation(operationType.type),
  822. defaults?.[fieldName] ?? defaults?.[rootName],
  823. {
  824. document,
  825. variables,
  826. cached: true,
  827. },
  828. )
  829. })
  830. }
  831. return query
  832. }