introspection.lua 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. local path = (...):gsub('%.[^%.]+$', '')
  2. local types = require(path .. '.types')
  3. local util = require(path .. '.util')
  4. local cjson = require 'cjson' -- needs to be cloned from here https://github.com/openresty/lua-cjson for cjson.empty_array feature
  5. local function isNullish(value)
  6. return value == nil
  7. end
  8. local function instanceof(t, s)
  9. return t.__type == s
  10. end
  11. local function resolveDirective(directive)
  12. local res = {}
  13. if directive.onQuery then table.insert(res, 'QUERY') end
  14. if directive.onMutation then table.insert(res, 'MUTATION') end
  15. if directive.onField then table.insert(res, 'FIELD') end
  16. if directive.onFragmentDefinition then table.insert(res, 'FRAGMENT_DEFINITION') end
  17. if directive.onFragmentSpread then table.insert(res, 'FRAGMENT_SPREAD') end
  18. if directive.onInlineFragment then table.insert(res, 'INLINE_FRAGMENT') end
  19. return res
  20. end
  21. local function mapToList(m)
  22. local r = {}
  23. for k,v in pairs(m) do
  24. table.insert(r, v)
  25. end
  26. return r
  27. end
  28. local __Directive, __DirectiveLocation, __Type, __Field, __InputValue,__EnumValue, TypeKind, __TypeKind, SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, astFromValue, printAst, printers
  29. local __Schema = types.object({
  30. name = '__Schema',
  31. description = [[
  32. A GraphQL Schema defines the capabilities of a GraphQL server. It
  33. exposes all available types and directives on the server, as well as
  34. the entry points for query and mutation operations.
  35. ]],
  36. fields = function()
  37. return {
  38. types = {
  39. description = 'A list of all types supported by this server.',
  40. kind = types.nonNull(types.list(types.nonNull(__Type))),
  41. resolve = function(schema)
  42. local typeMap = schema:getTypeMap()
  43. local res = {}
  44. for k, v in pairs(typeMap) do
  45. table.insert(res, typeMap[k])
  46. end
  47. return res
  48. end
  49. },
  50. queryType = {
  51. description = 'The type that query operations will be rooted at.',
  52. kind = types.nonNull(__Type),
  53. resolve = function(schema)
  54. return schema:getQueryType()
  55. end
  56. },
  57. mutationType = {
  58. description = 'If this server supports mutation, the type that mutation operations will be rooted at.',
  59. kind = __Type,
  60. resolve = function(schema)
  61. return schema:getMutationType()
  62. end
  63. },
  64. directives = {
  65. description = 'A list of all directives supported by this server.',
  66. kind = types.nonNull(types.list(types.nonNull(__Directive))),
  67. resolve = function(schema)
  68. return schema.directives
  69. end
  70. }
  71. }
  72. end
  73. })
  74. __Directive = types.object({
  75. name = '__Directive',
  76. description = [[
  77. A Directive provides a way to describe alternate runtime execution and
  78. type validation behavior in a GraphQL document.
  79. \n\nIn some cases, you need to provide options to alter GraphQL’s
  80. execution behavior in ways field arguments will not suffice, such as
  81. conditionally including or skipping a field. Directives provide this by
  82. describing additional information to the executor.
  83. ]],
  84. fields = function()
  85. return {
  86. name = types.nonNull(types.string),
  87. description = types.string,
  88. locations = {
  89. kind = types.nonNull(types.list(types.nonNull(
  90. __DirectiveLocation
  91. )))
  92. },
  93. args = {
  94. kind = types.nonNull(types.list(types.nonNull(__InputValue))),
  95. resolve = function(field)
  96. local args = {}
  97. local transform = function(a, n)
  98. if a.__type then
  99. return { kind = a, name = n }
  100. else
  101. if a.name then return a end
  102. local r = { name = n }
  103. for k,v in pairs(a) do
  104. r[k] = v
  105. end
  106. return r
  107. end
  108. end
  109. for k, v in pairs(field.arguments or {}) do
  110. table.insert(args, transform(v, k))
  111. end
  112. if #args > 0 then
  113. return args
  114. else
  115. return cjson.empty_array
  116. end
  117. end
  118. }
  119. }
  120. end
  121. })
  122. __DirectiveLocation = types.enum({
  123. name = '__DirectiveLocation',
  124. description = [[
  125. A Directive can be adjacent to many parts of the GraphQL language, a
  126. __DirectiveLocation describes one such possible adjacencies.
  127. ]],
  128. values = {
  129. QUERY = {
  130. value = 'QUERY',
  131. description = 'Location adjacent to a query operation.'
  132. },
  133. MUTATION = {
  134. value = 'MUTATION',
  135. description = 'Location adjacent to a mutation operation.'
  136. },
  137. FIELD = {
  138. value = 'FIELD',
  139. description = 'Location adjacent to a field.'
  140. },
  141. FRAGMENT_DEFINITION = {
  142. value = 'FRAGMENT_DEFINITION',
  143. description = 'Location adjacent to a fragment definition.'
  144. },
  145. FRAGMENT_SPREAD = {
  146. value = 'FRAGMENT_SPREAD',
  147. description = 'Location adjacent to a fragment spread.'
  148. },
  149. INLINE_FRAGMENT = {
  150. value = 'INLINE_FRAGMENT',
  151. description = 'Location adjacent to an inline fragment.'
  152. }
  153. }
  154. })
  155. __Type = types.object({
  156. name = '__Type',
  157. description =
  158. 'The fundamental unit of any GraphQL Schema is the type. There are ' ..
  159. 'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' ..
  160. '\n\nDepending on the kind of a type, certain fields describe ' ..
  161. 'information about that type. Scalar types provide no information ' ..
  162. 'beyond a name and description, while Enum types provide their values. ' ..
  163. 'Object and Interface types provide the fields they describe. Abstract ' ..
  164. 'types, Union and Interface, provide the Object types possible ' ..
  165. 'at runtime. List and NonNull types compose other types.',
  166. fields = function() return {
  167. kind = {
  168. kind = types.nonNull(__TypeKind),
  169. resolve = function (type)
  170. if instanceof(type, 'Scalar') then
  171. return TypeKind.SCALAR;
  172. elseif instanceof(type, 'Object') then
  173. return TypeKind.OBJECT;
  174. elseif instanceof(type, 'Interface') then
  175. return TypeKind.INTERFACE;
  176. elseif instanceof(type, 'Union') then
  177. return TypeKind.UNION;
  178. elseif instanceof(type, 'Enum') then
  179. return TypeKind.ENUM;
  180. elseif instanceof(type, 'InputObject') then
  181. return TypeKind.INPUT_OBJECT;
  182. elseif instanceof(type, 'List') then
  183. return TypeKind.LIST;
  184. elseif instanceof(type, 'NonNull') then
  185. return TypeKind.NON_NULL;
  186. end
  187. error('Unknown kind of kind = ' .. type);
  188. end
  189. },
  190. name = types.string,
  191. description = types.string,
  192. fields = {
  193. kind = types.list(types.nonNull(__Field)),
  194. arguments = {
  195. -- includeDeprecated = types.boolean
  196. includeDeprecated = { kind = types.boolean, defaultValue = false }
  197. },
  198. resolve = function(t, args)
  199. if instanceof(t, 'Object') or
  200. instanceof(t, 'Interface') then
  201. local fieldMap = t.fields;
  202. local fields = {}; for k,v in pairs(fieldMap) do table.insert(fields, fieldMap[k]) end
  203. if not args.includeDeprecated then
  204. fields = util.filter(fields, function(field) return not field.deprecationReason end);
  205. end
  206. if #fields > 0 then return fields else return cjson.empty_array end
  207. end
  208. return nil;
  209. end
  210. },
  211. interfaces = {
  212. kind = types.list(types.nonNull(__Type)),
  213. resolve = function(type)
  214. if instanceof(type, 'Object') then
  215. return type.interfaces and type.interfaces or cjson.empty_array;
  216. end
  217. end
  218. },
  219. possibleTypes = {
  220. kind = types.list(types.nonNull(__Type)),
  221. resolve = function(type, args, context, obj)
  222. if instanceof(type, 'Interface') or
  223. instanceof(type, 'Union') then
  224. return context.schema:getPossibleTypes(type);
  225. end
  226. end
  227. },
  228. enumValues = {
  229. kind = types.list(types.nonNull(__EnumValue)),
  230. arguments = {
  231. -- includeDeprecated = types.boolean
  232. includeDeprecated = { kind = types.boolean, defaultValue = false }
  233. },
  234. resolve = function(type, args)
  235. if instanceof(type, 'Enum') then
  236. local values = type.values;
  237. if not args.includeDeprecated then
  238. values = util.filter(values, function(value) return not value.deprecationReason end);
  239. end
  240. return mapToList(values);
  241. end
  242. end
  243. },
  244. inputFields = {
  245. kind = types.list(types.nonNull(__InputValue)),
  246. resolve = function(type)
  247. if instanceof(type, 'InputObject') then
  248. local fieldMap = type.fields;
  249. local fields = {}; for k,v in pairs(fieldMap) do table.insert(fields, fieldMap[k]) end; return fields
  250. end
  251. end
  252. },
  253. ofType = { kind = __Type }
  254. } end
  255. });
  256. __Field = types.object({
  257. name = '__Field',
  258. description =
  259. 'Object and Interface types are described by a list of Fields, each of ' ..
  260. 'which has a name, potentially a list of arguments, and a return type.',
  261. fields = function() return {
  262. name = types.nonNull(types.string),
  263. description = types.string,
  264. args = {
  265. -- kind = types.list(__InputValue),
  266. kind = types.nonNull(types.list(types.nonNull(__InputValue))),
  267. resolve = function(field)
  268. local args = {}
  269. local transform = function(a, n)
  270. if a.__type then
  271. return {kind = a, name = n}
  272. else
  273. if not a.name then
  274. local r = {name = n}
  275. for k,v in pairs(a) do
  276. r[k] = v
  277. end
  278. return r
  279. else
  280. return a
  281. end
  282. end
  283. end
  284. for k, v in pairs(field.arguments or {}) do table.insert(args, transform(v, k)) end
  285. -- return args
  286. if #args > 0 then return args else return cjson.empty_array end
  287. end
  288. },
  289. type = { kind = types.nonNull(__Type), resolve = function(field) return field.kind end },
  290. isDeprecated = {
  291. kind = types.nonNull(types.boolean),
  292. resolve = function(field) return not isNullish(field.deprecationReason) end
  293. },
  294. deprecationReason = types.string
  295. } end
  296. });
  297. __InputValue = types.object({
  298. name = '__InputValue',
  299. description =
  300. 'Arguments provided to Fields or Directives and the input fields of an ' ..
  301. 'InputObject are represented as Input Values which describe their type ' ..
  302. 'and optionally a default value.',
  303. fields = function() return {
  304. name = types.nonNull(types.string),
  305. description = types.string,
  306. type = { kind = types.nonNull(__Type), resolve = function(field) return field.kind end },
  307. defaultValue = {
  308. kind = types.string,
  309. description =
  310. 'A GraphQL-formatted string representing the default value for this ' ..
  311. 'input value.',
  312. resolve = function(inputVal) if isNullish(inputVal.defaultValue)
  313. then return nil
  314. else return printAst(astFromValue(inputVal.defaultValue, inputVal)) end end
  315. }
  316. } end
  317. });
  318. __EnumValue = types.object({
  319. name = '__EnumValue',
  320. description =
  321. 'One possible value for a given Enum. Enum values are unique values, not ' ..
  322. 'a placeholder for a string or numeric value. However an Enum value is ' ..
  323. 'returned in a JSON response as a string.',
  324. fields = function() return {
  325. name = types.nonNull(types.string),
  326. description = types.string,
  327. isDeprecated = {
  328. kind = types.nonNull(types.boolean),
  329. resolve = function(enumValue) return not isNullish(enumValue.deprecationReason) end
  330. },
  331. deprecationReason =
  332. types.string
  333. } end
  334. });
  335. TypeKind = {
  336. SCALAR = 'SCALAR',
  337. OBJECT = 'OBJECT',
  338. INTERFACE = 'INTERFACE',
  339. UNION = 'UNION',
  340. ENUM = 'ENUM',
  341. INPUT_OBJECT = 'INPUT_OBJECT',
  342. LIST = 'LIST',
  343. NON_NULL = 'NON_NULL'
  344. };
  345. __TypeKind = types.enum({
  346. name = '__TypeKind',
  347. description = 'An enum describing what kind of type a given `__Type` is.',
  348. values = {
  349. SCALAR = {
  350. value = TypeKind.SCALAR,
  351. description = 'Indicates this type is a scalar.'
  352. },
  353. OBJECT = {
  354. value = TypeKind.OBJECT,
  355. description = 'Indicates this type is an object. ' ..
  356. '`fields` and `interfaces` are valid fields.'
  357. },
  358. INTERFACE = {
  359. value = TypeKind.INTERFACE,
  360. description = 'Indicates this type is an interface. ' ..
  361. '`fields` and `possibleTypes` are valid fields.'
  362. },
  363. UNION = {
  364. value = TypeKind.UNION,
  365. description = 'Indicates this type is a union. ' ..
  366. '`possibleTypes` is a valid field.'
  367. },
  368. ENUM = {
  369. value = TypeKind.ENUM,
  370. description = 'Indicates this type is an enum. ' ..
  371. '`enumValues` is a valid field.'
  372. },
  373. INPUT_OBJECT = {
  374. value = TypeKind.INPUT_OBJECT,
  375. description = 'Indicates this type is an input object. ' ..
  376. '`inputFields` is a valid field.'
  377. },
  378. LIST = {
  379. value = TypeKind.LIST,
  380. description = 'Indicates this type is a list. ' ..
  381. '`ofType` is a valid field.'
  382. },
  383. NON_NULL = {
  384. value = TypeKind.NON_NULL,
  385. description = 'Indicates this type is a non-null. ' ..
  386. '`ofType` is a valid field.'
  387. }
  388. }
  389. });
  390. --
  391. -- Note that these are GraphQLFieldDefinition and not GraphQLFieldConfig,
  392. -- so the format for args is different.
  393. --
  394. SchemaMetaFieldDef = {
  395. name = '__schema',
  396. kind = types.nonNull(__Schema),
  397. description = 'Access the current type schema of this server.',
  398. arguments = {},
  399. resolve = function(source, args, context, obj) return context.schema end
  400. };
  401. TypeMetaFieldDef = {
  402. name = '__type',
  403. kind = __Type,
  404. description = 'Request the type information of a single type.',
  405. arguments = {
  406. name = types.nonNull(types.string)
  407. }
  408. --,resolve = function(source, { name } = { name = string }, context, { schema })
  409. -- return schema.getType(name) end
  410. };
  411. TypeNameMetaFieldDef = {
  412. name = '__typename',
  413. kind = types.nonNull(types.string),
  414. description = 'The name of the current Object type at runtime.',
  415. arguments = {},
  416. resolve = function(source, args, context, obj) return obj.parentType.name end
  417. };
  418. -- Produces a GraphQL Value AST given a lua value.
  419. -- Optionally, a GraphQL type may be provided, which will be used to
  420. -- disambiguate between value primitives.
  421. -- | JSON Value | GraphQL Value |
  422. -- | ------------- | -------------------- |
  423. -- | Object | Input Object |
  424. -- | Array | List |
  425. -- | Boolean | Boolean |
  426. -- | String | String / Enum Value |
  427. -- | Number | Int / Float |
  428. local Kind = {
  429. LIST = 'ListValue',
  430. BOOLEAN = 'BooleanValue',
  431. FLOAT = 'FloatValue',
  432. INT = 'IntValue',
  433. FLOAT = 'FloatValue',
  434. ENUM = 'EnumValue',
  435. STRING = 'StringValue',
  436. OBJECT_FIELD = 'ObjectField',
  437. NAME = 'Name',
  438. OBJECT = 'ObjectValue'
  439. }
  440. printers = {
  441. IntValue = function(v) return v.value end,
  442. FloatValue = function(v) return v.value end,
  443. StringValue = function(v) return cjson.encode(v.value) end,
  444. BooleanValue = function(v) return cjson.encode(v.value) end,
  445. EnumValue = function(v) return v.value end,
  446. ListValue = function(v) return '[' .. table.concat(util.map(v.values, printAst), ', ') .. ']' end,
  447. ObjectValue = function(v) return '{' .. table.concat(util.map(v.fields, printAst), ', ') .. '}' end,
  448. ObjectField = function(v) return v.name .. ': ' .. v.value end
  449. }
  450. printAst = function(v)
  451. return printers[v.kind](v)
  452. end
  453. astFromValue = function(value, tt)
  454. -- Ensure flow knows that we treat function params as const.
  455. local _value = value
  456. if instanceof(tt,'NonNull') then
  457. -- Note: we're not checking that the result is non-null.
  458. -- This function is not responsible for validating the input value.
  459. return astFromValue(_value, tt.ofType)
  460. end
  461. if isNullish(_value) then
  462. return nil
  463. end
  464. -- Convert JavaScript array to GraphQL list. If the GraphQLType is a list, but
  465. -- the value is not an array, convert the value using the list's item type.
  466. if type(_value) == 'table' and #_value > 0 then
  467. local itemType = instanceof(tt, 'List') and tt.ofType or nil
  468. return {
  469. kind = Kind.LIST,
  470. values = util.map(_value, function(item)
  471. local itemValue = astFromValue(item, itemType)
  472. assert(itemValue, 'Could not create AST item.')
  473. return itemValue
  474. end)
  475. }
  476. elseif instanceof(tt, 'List') then
  477. -- Because GraphQL will accept single values as a "list of one" when
  478. -- expecting a list, if there's a non-array value and an expected list type,
  479. -- create an AST using the list's item type.
  480. return astFromValue(_value, tt.ofType)
  481. end
  482. if type(_value) == 'boolean' then
  483. return { kind = Kind.BOOLEAN, value = _value }
  484. end
  485. -- JavaScript numbers can be Float or Int values. Use the GraphQLType to
  486. -- differentiate if available, otherwise prefer Int if the value is a
  487. -- valid Int.
  488. if type(_value) == 'number' then
  489. local stringNum = String(_value)
  490. local isIntValue = _value%1 == 0
  491. if isIntValue then
  492. if tt == types.float then
  493. return { kind = Kind.FLOAT, value = stringNum .. '.0' }
  494. end
  495. return { kind = Kind.INT, value = stringNum }
  496. end
  497. return { kind = Kind.FLOAT, value = stringNum }
  498. end
  499. -- JavaScript strings can be Enum values or String values. Use the
  500. -- GraphQLType to differentiate if possible.
  501. if type(_value) == 'string' then
  502. if instanceof(tt, 'Enum') and _value:match('/^[_a-zA-Z][_a-zA-Z0-9]*$/') then
  503. return { kind =Kind.ENUM, value = _value }
  504. end
  505. -- Use JSON stringify, which uses the same string encoding as GraphQL,
  506. -- then remove the quotes.
  507. return {
  508. kind = Kind.STRING,
  509. value = (cjson.encode(_value)):sub(1, -1)
  510. }
  511. end
  512. -- last remaining possible typeof
  513. assert(type(_value) == 'table')
  514. assert(_value ~= nil)
  515. -- Populate the fields of the input object by creating ASTs from each value
  516. -- in the JavaScript object.
  517. local fields = {}
  518. for fieldName,v in pairs(_value) do
  519. local fieldType
  520. if instanceof(tt, 'InputObject') then
  521. local fieldDef = tt.fields[fieldName];
  522. fieldType = fieldDef and fieldDef.kind;
  523. end
  524. local fieldValue = astFromValue(_value[fieldName], fieldType);
  525. if fieldValue then
  526. table.insert(fields, {
  527. kind = Kind.OBJECT_FIELD,
  528. name = { kind = Kind.NAME, value = fieldName },
  529. value = fieldValue
  530. })
  531. end
  532. end
  533. return { kind = Kind.OBJECT, fields = fields }
  534. end
  535. return {
  536. __Schema = __Schema,
  537. __Directive = __Directive,
  538. __DirectiveLocation = __DirectiveLocation,
  539. __Type = __Type,
  540. __Field = __Field,
  541. __EnumValue = __EnumValue,
  542. TypeKind = TypeKind,
  543. __TypeKind = __TypeKind,
  544. SchemaMetaFieldDef = SchemaMetaFieldDef,
  545. TypeMetaFieldDef = TypeMetaFieldDef,
  546. TypeNameMetaFieldDef = TypeNameMetaFieldDef
  547. }