App.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import React, {useState, useEffect} from 'react';
  2. import logo from './logo.svg';
  3. import './App.css';
  4. import { GraphQLClient } from 'graphql-request';
  5. let gql = new GraphQLClient("/graphql", {headers: localStorage.authToken ? {Authorization: 'Bearer '+localStorage.authToken} : {}})
  6. //TODO: use graphql-tag to get types, or, at least detect query
  7. //for proper binding between model objects and result of gql query
  8. async function createModels(gql, config={create: 'Upsert', update: 'Upsert', delete: "Delete", findOne: "FindOne", find: 'Find', count: 'Count'}){
  9. const getQuery = name =>
  10. `query mutations{
  11. __type(name: "${name}") {
  12. name,
  13. kind,
  14. fields {
  15. name,
  16. type {
  17. kind,
  18. ofType{
  19. name,
  20. fields{
  21. name,
  22. type{
  23. name,
  24. kind
  25. }
  26. }
  27. }
  28. name,
  29. fields{
  30. name,
  31. type{
  32. name,
  33. kind
  34. }
  35. }
  36. }
  37. args{
  38. name,
  39. type{
  40. name,
  41. inputFields{
  42. name,
  43. type{
  44. name,
  45. kind,
  46. ofType{
  47. name
  48. }
  49. }
  50. }
  51. }
  52. }
  53. }
  54. }
  55. }`
  56. let { __type: { fields: mutations }} = await gql.request(getQuery('Mutation'))
  57. let { __type: { fields: queries }} = await gql.request(getQuery('Query'))
  58. console.log(mutations)
  59. console.log(queries)
  60. let classes = {}
  61. const createClass = (name, fields) => {
  62. if (!(name in classes)) {
  63. classes[name] = class {
  64. constructor(data){
  65. Object.assign(this, data)
  66. }
  67. static get fields(){
  68. return fields
  69. }
  70. }
  71. Object.defineProperty(classes[name], 'name', {value: name})
  72. }
  73. return classes[name]
  74. }
  75. for (let query of queries){
  76. let className = query.type.name || query.type.ofType.name
  77. if (className === 'Int'){ //TODO: fuck this
  78. className = query.name.match(new RegExp(`([A-Za-z]+)(${Object.values(config).join('|')})`))[1]
  79. }
  80. let classFields = query.type.fields || query.type.ofType && query.type.ofType.fields
  81. console.log(classFields)
  82. let _class = createClass(className, classFields)
  83. for (let [method, methodName] of Object.entries(config)){
  84. if (!_class[method] && query.name.includes(methodName)){
  85. let methods = {
  86. find(q, cursorCalls={}){
  87. let queryText = `query FMFind($q:${query.args[0].type.name}){
  88. ${query.name}(${query.args[0].name}: $q)
  89. {${classFields.filter(x => x.type.kind === 'SCALAR').map(x => x.name).join(',')}}
  90. }
  91. `
  92. console.log(queryText, {q: JSON.stringify([q, cursorCalls])})
  93. return gql.request(queryText, {q: JSON.stringify([q, cursorCalls])}).then(data =>{
  94. return data[query.name].map(record => new _class(record))
  95. })
  96. },
  97. findOne(query){
  98. },
  99. count(q, cursorCalls={}){
  100. let queryText = `query FMFind($q:${query.args[0].type.name}){
  101. ${query.name}(${query.args[0].name}: $q)
  102. }
  103. `
  104. return gql.request(queryText, {q: JSON.stringify([q, cursorCalls])}).then(data =>{
  105. return data[query.name]
  106. })
  107. }
  108. }
  109. _class[method] = methods[method]
  110. }
  111. }
  112. }
  113. return classes
  114. }
  115. async function createModels2(gql, config={create: 'Upsert', update: 'Upsert', delete: "Delete", findOne: "FindOne", find: 'Find', count: 'Count'}){
  116. const universeQuery = `query universe{
  117. __schema{
  118. types {
  119. name,
  120. kind,
  121. inputFields{
  122. name,
  123. type {
  124. kind,
  125. ofType{
  126. name,
  127. fields{
  128. name,
  129. type{
  130. name,
  131. kind
  132. }
  133. }
  134. }
  135. name,
  136. fields{
  137. name,
  138. type{
  139. name,
  140. kind
  141. }
  142. }
  143. }
  144. }
  145. fields {
  146. name,
  147. type {
  148. kind,
  149. ofType{
  150. name,
  151. fields{
  152. name,
  153. type{
  154. name,
  155. kind
  156. }
  157. }
  158. }
  159. name,
  160. fields{
  161. name,
  162. type{
  163. name,
  164. kind
  165. }
  166. }
  167. }
  168. args{
  169. name,
  170. type{
  171. name,
  172. inputFields{
  173. name,
  174. type{
  175. name,
  176. kind,
  177. ofType{
  178. name
  179. }
  180. }
  181. }
  182. }
  183. }
  184. }
  185. }
  186. }}
  187. `
  188. const universe = await gql.request(universeQuery)
  189. console.log(universe)
  190. const types = []
  191. const inputs = []
  192. for (let type of universe.__schema.types){
  193. if (!["Query", "Mutation"].includes(type.name) && type.kind === "OBJECT" && !type.name.startsWith('__')) types.push(type)
  194. if (!["Query", "Mutation"].includes(type.name) && type.kind === "INPUT_OBJECT" && !type.name.startsWith('__')) inputs.push(type)
  195. }
  196. console.log(types, inputs)
  197. let classes = {}
  198. const inTypes = name => types.find(type => type.name === name)
  199. const inInputs = name => inputs.find(type => type.name === name)
  200. const projectionBuilder = (type, allFields=true) => {
  201. if (!allFields && type.fields[0].name !== '_id') allFields = true
  202. if (allFields)
  203. return '{' +
  204. type.fields.map(field => {
  205. return field.name + ((field.type.kind === 'OBJECT' && (inTypes(field.type.name)) && projectionBuilder(inTypes(field.type.name), false)) ||
  206. (field.type.kind === 'LIST' && (inTypes(field.type.ofType.name)) && projectionBuilder(inTypes(field.type.ofType.name), false)) || '')
  207. })
  208. .join(',')
  209. + '}'
  210. else return `{_id}`
  211. }
  212. const identityMap = {}
  213. const createClass = (name, type, input) => {
  214. if (!(name in classes)) {
  215. classes[name] = class {
  216. constructor(data={}, empty = false){
  217. if (data._id && data._id in identityMap)
  218. return identityMap[data._id]
  219. this.populate(data)
  220. this.empty = empty
  221. if (this._id) identityMap[this._id] = this
  222. }
  223. populate(data){
  224. type.fields.forEach(({name, type:{ ofType, kind, name:otherName}}) => {
  225. ({SCALAR(){
  226. if (data && typeof data === 'object' && name in data) this[name] = data[name]
  227. },
  228. LIST(){
  229. const otherType = inTypes(ofType.name)
  230. if (otherType && data[name])
  231. this[name] = data[name].map(otherEntity => new classes[otherType.name](otherEntity, otherEntity._id && Object.keys(otherEntity).length === 1))
  232. else if (data && typeof data === 'object' && name in data) this[name] = data[name]
  233. },
  234. OBJECT(){
  235. const otherType = inTypes(otherName)
  236. if (otherType && data[name])
  237. this[name] = new classes[otherType.name](data[name], data[name]._id && Object.keys(data[name]).length === 1)
  238. else if (data && typeof data === 'object' && name in data) this[name] = data[name]
  239. }})[kind].call(this)
  240. })
  241. }
  242. get empty(){
  243. return this.then
  244. }
  245. set empty(value){
  246. if (value){
  247. this.then = async (onFullfilled, onRejected) => {
  248. const gqlQuery = `
  249. query ${name}FindOne($query: String){
  250. ${name}FindOne(query: $query)
  251. ${projectionBuilder(type)}
  252. }
  253. `
  254. const data = await gql.request(gqlQuery, {query: JSON.stringify([{_id: this._id}])})
  255. this.populate(data[name + 'FindOne'])
  256. this.empty = false
  257. const thenResult = onFullfilled(this)
  258. if (thenResult && typeof thenResult === 'object' && typeof thenResult.then === 'function'){
  259. return await thenResult
  260. }
  261. return thenResult
  262. }
  263. }
  264. else delete this.then
  265. }
  266. static get type(){
  267. return type
  268. }
  269. static get input(){
  270. return input
  271. }
  272. async save(){
  273. if (this.empty) throw new ReferenceError('Cannot save empty object')
  274. const data = {}
  275. input.inputFields.forEach(({name, type:{ ofType, kind, name:otherName}}) => {
  276. ({SCALAR(){
  277. if (this[name]) data[name] = this[name]
  278. },
  279. LIST(){
  280. const otherType = inInputs(ofType.name)
  281. if (otherType && this[name] && this[name] instanceof Array)
  282. data[name] = this[name].map(otherEntity => (otherEntity._id ? {_id: otherEntity._id} : otherEntity))
  283. },
  284. INPUT_OBJECT(){
  285. const otherType = inInputs(otherName)
  286. if (otherType && this[name] && typeof this[name] === 'object')
  287. data[name] = (this[name]._id ? {_id: this[name]._id} : this[name])
  288. }})[kind].call(this)
  289. })
  290. const gqlQuery = `
  291. mutation ${name}Upsert($data: ${input.name}){
  292. ${name}Upsert(${name.toLowerCase()}: $data)
  293. ${projectionBuilder(type)}
  294. }
  295. `
  296. let result = await gql.request(gqlQuery, {data})
  297. this.populate(result)
  298. }
  299. static async find(query){
  300. const gqlQuery = `
  301. query ${name}Find($query: String){
  302. ${name}Find(query: $query)
  303. ${projectionBuilder(type)}
  304. }
  305. `
  306. let result = await gql.request(gqlQuery, {query: JSON.stringify(query)})
  307. return result[name + 'Find'].map(entity => new classes[name](entity))
  308. }
  309. }
  310. Object.defineProperty(classes[name], 'name', {value: name})
  311. }
  312. return classes[name]
  313. }
  314. types.forEach((type) => createClass(type.name, type, inputs.find(input => input.name === `${type.name}Input`)))
  315. console.log(classes)
  316. classes.Good.find([{}]).then(goods => console.log(goods))
  317. }
  318. const dataReader = async () => {
  319. const { PriceItem } = await createModels(gql)
  320. let data = await PriceItem.find({}, {limit: [10]})
  321. return {data, PriceItem }
  322. }
  323. const Th = p =>
  324. <div className='Th' {...p} />
  325. const Row = ({top, children}) =>
  326. <div className='Row' style={{position: 'relative',top}}>
  327. {children}
  328. </div>
  329. const Cell = ({children, ...props}) =>
  330. <div className='Cell' {...props}>
  331. {children}
  332. </div>
  333. function ModelGrid({model, rowHeight, gridHeight, overload, query, cellDoubleClick}){
  334. const onScreenRowCount = gridHeight/rowHeight
  335. const overloadedRowCount = overload * onScreenRowCount
  336. const [oldQuery, setOldQuery] = useState()
  337. const jsonQuery = JSON.stringify(query)
  338. const [totalCount, setTotalCount] = useState()
  339. if (!totalCount || oldQuery !== jsonQuery) {
  340. model && model.count(query).then(count => { setTotalCount(count); setOldQuery(jsonQuery)})
  341. }
  342. const [gridContentRef, setGridContentRef] = useState()
  343. const [scroll, setScroll] = useState(0)
  344. const onScreenFirstRowIndex = Math.floor(scroll/rowHeight)
  345. let skip = onScreenFirstRowIndex - (overloadedRowCount - onScreenRowCount)/2;
  346. if (skip < 0) skip = 0
  347. const [sort, setSort] = useState([])
  348. const [records, setRecords] = useState()
  349. const cursorCalls = {skip: [skip], limit: [overloadedRowCount]}
  350. if (!model) return <>No model</>;
  351. if (sort.length) cursorCalls.sort = [sort]
  352. if (!records || oldQuery !== jsonQuery) {
  353. model.find(query, cursorCalls)
  354. .then(records => (setRecords(records), setOldQuery(jsonQuery)))
  355. }
  356. return(
  357. <>
  358. <div className='GridHeader'>
  359. {model.fields.map(field => <Th onClick={e => (setSort(field.name === sort[0] ? [sort[0], -sort[1]] : [field.name, 1]), setRecords(null))}>{field.name}</Th>)}
  360. </div>
  361. <div className='GridViewport'
  362. onScroll={e => {
  363. if (Math.abs(e.target.scrollTop - scroll) > gridHeight*overload/3){
  364. setScroll(e.target.scrollTop);
  365. setRecords(null)
  366. } }}
  367. style={{maxHeight: gridHeight, height: gridHeight}}>
  368. <div className='GridContent'
  369. style={{height: totalCount*rowHeight}}
  370. ref={e => setGridContentRef(e)}>
  371. {records && records.map((record,i) =>
  372. <Row top={(skip)*rowHeight}>
  373. {model.fields.map(field =>
  374. <Cell onDoubleClick={e => cellDoubleClick(field.name, record[field.name], record._id)}>
  375. {record[field.name]}
  376. </Cell>)}
  377. </Row>)}
  378. </div>
  379. </div>
  380. </>
  381. );
  382. }
  383. function App() {
  384. let [models, setModels] = useState()
  385. let [queryText, setQueryText] = useState('{manufacturerName: "TOYOTA"}')
  386. let [query, setQuery] = useState({})
  387. models || createModels2(gql).then(models => setModels(models))
  388. console.log(query)
  389. console.log('MODELS', models)
  390. return (
  391. <div className="App">
  392. {models && Object.entries(models).map(([key, model]) => <div>{key}</div>)}
  393. {models && <ModelGrid model={models.jk} rowHeight={50} gridHeight={700} overload={5} query={query}
  394. cellDoubleClick={(name, text, _id) => setQueryText(`{${name}:\`${text}\`}`)}/> }
  395. <textarea onChange={e => setQueryText(e.target.value)} value={queryText} />
  396. <button onClick={() => setQuery(eval(`(${queryText})`))}>Run</button>
  397. </div>
  398. );
  399. }
  400. export default App;