1: -module(graphql_vcard_SUITE). 2: 3: -compile([export_all, nowarn_export_all]). 4: 5: -import(distributed_helper, [require_rpc_nodes/1]). 6: -import(graphql_helper, [execute_user/3, execute_auth/2, user_to_bin/1]). 7: 8: -include_lib("common_test/include/ct.hrl"). 9: -include_lib("eunit/include/eunit.hrl"). 10: -include_lib("exml/include/exml.hrl"). 11: -include_lib("escalus/include/escalus.hrl"). 12: -include_lib("../../include/mod_roster.hrl"). 13: 14: suite() -> 15: require_rpc_nodes([mim]) ++ escalus:suite(). 16: 17: all() -> 18: [{group, user_vcard}, {group, admin_vcard}]. 19: 20: groups() -> 21: [{user_vcard, [], user_vcard_handler()}, 22: {admin_vcard, [], admin_vcard_handler()}]. 23: 24: user_vcard_handler() -> 25: [user_set_vcard, 26: user_get_their_vcard, 27: user_get_their_vcard_no_vcard, 28: user_get_others_vcard, 29: user_get_others_vcard_no_user, 30: user_get_others_vcard_no_vcard]. 31: 32: admin_vcard_handler() -> 33: [admin_set_vcard, 34: admin_set_vcard_incomplete_fields, 35: admin_set_vcard_no_user, 36: admin_get_vcard, 37: admin_get_vcard_no_vcard, 38: admin_get_vcard_no_user]. 39: 40: init_per_suite(Config) -> 41: case vcard_helper:is_vcard_ldap() of 42: true -> 43: {skip, ldap_vcard_is_not_supported}; 44: _ -> 45: Config2 = escalus:init_per_suite(Config), 46: dynamic_modules:save_modules(domain_helper:host_type(), Config2) 47: end. 48: 49: end_per_suite(Config) -> 50: dynamic_modules:restore_modules(Config), 51: escalus:end_per_suite(Config). 52: 53: init_per_group(admin_vcard, Config) -> 54: graphql_helper:init_admin_handler(Config); 55: init_per_group(user_vcard, Config) -> 56: [{schema_endpoint, user} | Config]. 57: 58: end_per_group(admin_vcard, _Config) -> 59: escalus_fresh:clean(); 60: end_per_group(user_vcard, _Config) -> 61: escalus_fresh:clean(). 62: 63: init_per_testcase(CaseName, Config) -> 64: escalus:init_per_testcase(CaseName, Config). 65: 66: end_per_testcase(CaseName, Config) -> 67: escalus:end_per_testcase(CaseName, Config). 68: 69: % User test cases 70: 71: user_set_vcard(Config) -> 72: escalus:fresh_story_with_config(Config, [{alice, 1}], 73: fun user_set_vcard/2). 74: 75: user_set_vcard(Config, Alice) -> 76: Vcard = complete_vcard_input(), 77: BodySet = set_vcard_body_user(Vcard), 78: GraphQlRequestSet = execute_user(BodySet, Alice, Config), 79: ParsedResultSet = ok_result(<<"vcard">>, <<"setVcard">>, GraphQlRequestSet), 80: ?assertEqual(Vcard, ParsedResultSet), 81: QueryGet = user_get_full_vcard_as_result_query(), 82: BodyGet = #{query => QueryGet, operationName => <<"Q1">>, variables => #{}}, 83: GraphQlRequestGet = execute_user(BodyGet, Alice, Config), 84: ParsedResultGet = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequestGet), 85: ?assertEqual(Vcard, ParsedResultGet). 86: 87: user_get_their_vcard(Config) -> 88: escalus:fresh_story_with_config(Config, [{alice, 1}], 89: fun user_get_their_vcard/2). 90: 91: user_get_their_vcard(Config, Alice) -> 92: Client1Fields = [{<<"FN">>, <<"TESTNAME">>}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL">>}, 93: {<<"HOME">>, []}, {"WORK", []}]}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL2">>}, 94: {<<"HOME">>, []}]}], 95: ExpectedResult = #{<<"formattedName">> => <<"TESTNAME">>, 96: <<"email">> => [#{<<"userId">> => <<"TESTEMAIL">>, <<"tags">> => [<<"HOME">>, <<"WORK">>]}, 97: #{<<"userId">> => <<"TESTEMAIL2">>, <<"tags">> => [<<"HOME">>]}]}, 98: escalus_client:send_and_wait(Alice, escalus_stanza:vcard_update(Client1Fields)), 99: Body = #{query => user_get_query(), operationName => <<"Q1">>, variables => #{}}, 100: GraphQlRequest = execute_user(Body, Alice, Config), 101: ParsedResult = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequest), 102: ?assertEqual(ExpectedResult, ParsedResult). 103: 104: user_get_their_vcard_no_vcard(Config) -> 105: escalus:fresh_story_with_config(Config, [{alice, 1}], 106: fun user_get_their_vcard_no_vcard/2). 107: 108: user_get_their_vcard_no_vcard(Config, Alice) -> 109: Body = #{query => user_get_query(), operationName => <<"Q1">>, variables => #{}}, 110: GraphQlRequest = execute_user(Body, Alice, Config), 111: ParsedResult = error_result(<<"message">>, GraphQlRequest), 112: ?assertEqual(<<"Vcard for user not found">>, ParsedResult). 113: 114: user_get_others_vcard(Config) -> 115: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], 116: fun user_get_others_vcard/3). 117: 118: user_get_others_vcard(Config, Alice, Bob) -> 119: Client1Fields = [{<<"FN">>, <<"TESTNAME">>}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL">>}, 120: {<<"HOME">>, []}, {"WORK", []}]}, {<<"EMAIL">>, [{<<"USERID">>, <<"TESTEMAIL2">>}, 121: {<<"HOME">>, []}]}], 122: ExpectedResult = #{<<"formattedName">> => <<"TESTNAME">>, 123: <<"email">> => [#{<<"userId">> => <<"TESTEMAIL">>, <<"tags">> => [<<"HOME">>, <<"WORK">>]}, 124: #{<<"userId">> => <<"TESTEMAIL2">>, <<"tags">> => [<<"HOME">>]}]}, 125: escalus_client:send_and_wait(Bob, escalus_stanza:vcard_update([{<<"VERSION">>, <<"TESTVERSION">>} | Client1Fields])), 126: Body = #{query => user_get_query(), operationName => <<"Q1">>, 127: variables => #{user => user_to_bin(Bob)}}, 128: GraphQlRequest = execute_user(Body, Alice, Config), 129: ParsedResult = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequest), 130: ?assertEqual(ExpectedResult, ParsedResult). 131: 132: user_get_others_vcard_no_vcard(Config) -> 133: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], 134: fun user_get_others_vcard_no_vcard/3). 135: 136: user_get_others_vcard_no_vcard(Config, Alice, Bob) -> 137: Body = #{query => user_get_query(), operationName => <<"Q1">>, 138: variables => #{user => user_to_bin(Bob)}}, 139: GraphQlRequest = execute_user(Body, Alice, Config), 140: ParsedResult = error_result(<<"message">>, GraphQlRequest), 141: ?assertEqual(<<"Vcard for user not found">>, ParsedResult). 142: 143: user_get_others_vcard_no_user(Config) -> 144: escalus:fresh_story_with_config(Config, [{alice, 1}], 145: fun user_get_others_vcard_no_user/2). 146: 147: user_get_others_vcard_no_user(Config, Alice) -> 148: Body = #{query => user_get_query(), operationName => <<"Q1">>, 149: variables => #{user => <<"AAAAA">>}}, 150: GraphQlRequest = execute_user(Body, Alice, Config), 151: ParsedResult = error_result(<<"message">>, GraphQlRequest), 152: ?assertEqual(<<"User does not exist">>, ParsedResult). 153: 154: %% Admin test cases 155: 156: admin_set_vcard(Config) -> 157: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], 158: fun admin_set_vcard/3). 159: 160: admin_set_vcard(Config, Alice, _Bob) -> 161: Vcard = complete_vcard_input(), 162: BodySet = set_vcard_body_admin(Vcard, user_to_bin(Alice)), 163: GraphQlRequestSet = execute_auth(BodySet, Config), 164: ParsedResultSet = ok_result(<<"vcard">>, <<"setVcard">>, GraphQlRequestSet), 165: ?assertEqual(Vcard, ParsedResultSet), 166: QueryGet = admin_get_full_vcard_as_result_query(), 167: VarsGet = #{user => user_to_bin(Alice)}, 168: BodyGet = #{query => QueryGet, operationName => <<"Q1">>, variables => VarsGet}, 169: GraphQlRequestGet = execute_auth(BodyGet, Config), 170: ParsedResultGet = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequestGet), 171: ?assertEqual(Vcard, ParsedResultGet). 172: 173: admin_set_vcard_incomplete_fields(Config) -> 174: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], 175: fun admin_set_vcard_incomplete_fields/3). 176: 177: admin_set_vcard_incomplete_fields(Config, Alice, _Bob) -> 178: QuerySet = admin_get_full_vcard_as_result_mutation(), 179: VcardInput = address_vcard_input(), 180: [VcardAddress] = maps:get(<<"address">> ,VcardInput), 181: Vcard = [maps:put(<<"pcode">>, null, VcardAddress)], 182: VarsSet = #{user => user_to_bin(Alice), vcard => VcardInput}, 183: BodySet = #{query => QuerySet, operationName => <<"M1">>, variables => VarsSet}, 184: GraphQlRequestSet = execute_auth(BodySet, Config), 185: ParsedResultSet = ok_result(<<"vcard">>, <<"setVcard">>, GraphQlRequestSet), 186: ?assertEqual(Vcard, maps:get(<<"address">>, ParsedResultSet)), 187: QueryGet = admin_get_full_vcard_as_result_query(), 188: VarsGet = #{user => user_to_bin(Alice)}, 189: BodyGet = #{query => QueryGet, operationName => <<"Q1">>, variables => VarsGet}, 190: GraphQlRequestGet = execute_auth(BodyGet, Config), 191: ParsedResultGet = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequestGet), 192: ?assertEqual(Vcard, maps:get(<<"address">>, ParsedResultGet)). 193: 194: admin_set_vcard_no_user(Config) -> 195: Query = admin_get_full_vcard_as_result_mutation(), 196: OpName = <<"M1">>, 197: Vcard = complete_vcard_input(), 198: Vars = #{user => <<"AAAAA">>, vcard => Vcard}, 199: Body = #{query => Query, operationName => OpName, variables => Vars}, 200: GraphQlRequest = execute_auth(Body, Config), 201: ParsedResult = error_result(<<"message">>, GraphQlRequest), 202: ?assertEqual(<<"User does not exist">>, ParsedResult). 203: 204: admin_get_vcard(Config) -> 205: escalus:fresh_story_with_config(Config, [{alice, 1}, {bob, 1}], 206: fun admin_get_vcard/3). 207: 208: admin_get_vcard(Config, Alice, _Bob) -> 209: Client1Fields = [{<<"ADR">>, [{<<"POBOX">>, <<"TESTPobox">>}, {<<"EXTADD">>, <<"TESTExtadd">>}, 210: {<<"STREET">>, <<"TESTStreet">>}, {<<"LOCALITY">>, <<"TESTLocality">>}, 211: {<<"REGION">>, <<"TESTRegion">>}, {<<"PCODE">>, <<"TESTPCODE">>}, 212: {<<"CTRY">>, <<"TESTCTRY">>}, {<<"HOME">>, []}, {<<"WORK">>, []}]}], 213: ExpectedResult = #{<<"address">> => [#{<<"pobox">> => <<"TESTPobox">>, 214: <<"extadd">> => <<"TESTExtadd">>, <<"street">> => <<"TESTStreet">>, 215: <<"locality">> => <<"TESTLocality">>, <<"region">> => <<"TESTRegion">>, 216: <<"pcode">> => <<"TESTPCODE">>, <<"country">> => <<"TESTCTRY">>, 217: <<"tags">> => [<<"HOME">>, <<"WORK">>]}]}, 218: escalus_client:send_and_wait(Alice, escalus_stanza:vcard_update(Client1Fields)), 219: Vars = #{user => user_to_bin(Alice)}, 220: Body = #{query => admin_get_address_query(), operationName => <<"Q1">>, variables => Vars}, 221: GraphQlRequest = execute_auth(Body, Config), 222: ParsedResult = ok_result(<<"vcard">>, <<"getVcard">>, GraphQlRequest), 223: ?assertEqual(ExpectedResult, ParsedResult). 224: 225: admin_get_vcard_no_vcard(Config) -> 226: escalus:fresh_story_with_config(Config, [{alice, 1}], 227: fun admin_get_vcard_no_vcard/2). 228: 229: admin_get_vcard_no_vcard(Config, Alice) -> 230: Query = admin_get_address_query(), 231: OpName = <<"Q1">>, 232: Vars = #{user => user_to_bin(Alice)}, 233: Body = #{query => Query, operationName => OpName, variables => Vars}, 234: GraphQlRequest = execute_auth(Body, Config), 235: ParsedResult = error_result(<<"message">>, GraphQlRequest), 236: ?assertEqual(<<"Vcard for user not found">>, ParsedResult). 237: 238: admin_get_vcard_no_user(Config) -> 239: Query = admin_get_address_query(), 240: OpName = <<"Q1">>, 241: Vars = #{user => <<"AAAAA">>}, 242: Body = #{query => Query, operationName => OpName, variables => Vars}, 243: GraphQlRequest = execute_auth(Body, Config), 244: ParsedResult = error_result(<<"message">>, GraphQlRequest), 245: ?assertEqual(<<"User does not exist">>, ParsedResult). 246: 247: %% Helpers 248: 249: ok_result(What1, What2, {{<<"200">>, <<"OK">>}, #{<<"data">> := Data}}) -> 250: maps:get(What2, maps:get(What1, Data)). 251: 252: error_result(What, {{<<"200">>, <<"OK">>}, #{<<"errors">> := [Data]}}) -> 253: maps:get(What, Data). 254: 255: set_vcard_body_user(Body) -> 256: QuerySet = user_get_full_vcard_as_result_mutation(), 257: #{query => QuerySet, operationName => <<"M1">>, variables => #{vcard => Body}}. 258: 259: set_vcard_body_admin(Body, User) -> 260: QuerySet = admin_get_full_vcard_as_result_mutation(), 261: #{query => QuerySet, operationName => <<"M1">>, variables => #{vcard => Body, user => User}}. 262: 263: address_vcard_input() -> 264: #{<<"formattedName">> => <<"TestName">>, 265: <<"nameComponents">> => #{}, 266: <<"address">> => [ 267: #{ 268: <<"tags">> => [<<"HOME">>, <<"WORK">>], 269: <<"pobox">> => <<"poboxTest">>, 270: <<"extadd">> => <<"extaddTest">>, 271: <<"street">> => <<"TESTSTREET123">>, 272: <<"locality">> => <<"LOCALITY123">>, 273: <<"region">> => <<"REGION777">>, 274: <<"country">> => <<"COUNTRY123">> 275: } 276: ]}. 277: 278: complete_vcard_input() -> 279: #{<<"formattedName">> => <<"TestName">>, 280: <<"nameComponents">> => #{ 281: <<"family">> => <<"familyName">>, 282: <<"givenName">> => <<"givenName">>, 283: <<"middleName">> => <<"middleName">>, 284: <<"prefix">> => <<"prefix">>, 285: <<"suffix">> => <<"sufix">> 286: }, 287: <<"nickname">> => [<<"NicknameTest">>, <<"SecondNickname">>], 288: <<"photo">> => [ 289: #{<<"type">> => <<"image/jpeg">>, 290: <<"binValue">> => <<"TestBinaries">>}, 291: #{<<"extValue">> => <<"External Value">>} 292: ], 293: <<"birthday">> => [<<"birthdayTest">>, <<"SecondBirthday">>], 294: <<"address">> => [ 295: #{ 296: <<"tags">> => [<<"HOME">>, <<"WORK">>], 297: <<"pobox">> => <<"poboxTest">>, 298: <<"extadd">> => <<"extaddTest">>, 299: <<"street">> => <<"TESTSTREET123">>, 300: <<"locality">> => <<"LOCALITY123">>, 301: <<"region">> => <<"REGION777">>, 302: <<"pcode">> => <<"PcodeTest">>, 303: <<"country">> => <<"COUNTRY123">> 304: }, 305: #{ 306: <<"tags">> => [<<"HOME">>, <<"WORK">>, <<"POSTAL">>], 307: <<"pobox">> => <<"poboxTestSecond">>, 308: <<"extadd">> => <<"extaddTestSecond">>, 309: <<"street">> => <<"TESTSTREET123Second">>, 310: <<"locality">> => <<"LOCALITY123Second">>, 311: <<"region">> => <<"REGION777TEST">>, 312: <<"pcode">> => <<"PcodeTestSECOND">>, 313: <<"country">> => <<"COUNTRY123SECOND">> 314: } 315: ], 316: <<"label">> => [ 317: #{<<"tags">> => [<<"WORK">>, <<"HOME">>], <<"line">> => [<<"LineTest">>, <<"AAA">>]}, 318: #{<<"tags">> => [<<"POSTAL">>, <<"WORK">>], <<"line">> => [<<"LineTest2">>, <<"AAA">>]} 319: ], 320: <<"telephone">> => [ 321: #{<<"tags">> => [<<"HOME">>, <<"BBS">>], <<"number">> => <<"590190190">>}, 322: #{<<"tags">> => [<<"WORK">>, <<"BBS">>], <<"number">> => <<"590190191">>} 323: ], 324: <<"email">> => [ 325: #{<<"tags">> => [<<"PREF">>], <<"userId">> => <<"userIDTEst">>}, 326: #{<<"tags">> => [<<"PREF">>, <<"HOME">>], <<"userId">> => <<"userIDTEsTETt">>} 327: ], 328: <<"jabberId">> => [<<"JabberId">>, <<"JabberIDSecind">>], 329: <<"mailer">> => [<<"MailerTest">>, <<"MailerSecond">>], 330: <<"timeZone">> => [<<"TimeZoneTest">>, <<"TimeZOneSecond">>], 331: <<"geo">> => [ 332: #{<<"lat">> => <<"LatitudeTest">>, <<"lon">> => <<"LongtitudeTest">>}, 333: #{<<"lat">> => <<"LatitudeTest2">>, <<"lon">> => <<"LongtitudeTest2">>} 334: ], 335: <<"title">> => [<<"TitleTest">>, <<"SEcondTest">>], 336: <<"role">> => [<<"roleTest">>, <<"SecondRole">>], 337: <<"logo">> => [ 338: #{<<"type">> => <<"image/jpeg">>, 339: <<"binValue">> => <<"TestBinariesLogo">>}, 340: #{<<"extValue">> => <<"External Value Logo">>} 341: ], 342: <<"agent">> => [ 343: #{<<"vcard">> => agent_vcard_input()}, 344: #{<<"extValue">> => <<"TESTVALUE">>}, 345: #{<<"vcard">> => agent_vcard_input()} 346: ], 347: <<"org">> => [ 348: #{<<"orgname">> => <<"TESTNAME">>, <<"orgunit">> => [<<"test1">>, <<"TEST2">>]}, 349: #{<<"orgname">> => <<"TESTNAME123">>, <<"orgunit">> => [<<"test1">>]} 350: ], 351: <<"categories">> => [ 352: #{<<"keyword">> => [<<"KeywordTest">>]}, 353: #{<<"keyword">> => [<<"KeywordTest">>, <<"Keyword2">>]} 354: ], 355: <<"note">> => [<<"NoteTest">>, <<"NOTE2">>], 356: <<"prodId">> => [<<"ProdIdTest">>, <<"PRodTEST2">>], 357: <<"rev">> => [<<"revTest">>, <<"RevTest2">>], 358: <<"sortString">> => [<<"sortStringTest">>, <<"String2">>], 359: <<"sound">> => [ 360: #{<<"binValue">> => <<"TestBinValue">>}, 361: #{<<"phonetic">> => <<"PhoneticTest">>}, 362: #{<<"extValue">> => <<"ExtValueTest">>} 363: ], 364: <<"uid">> => [<<"UidTest">>, <<"UID2">>], 365: <<"url">> => [<<"UrlTest">>, <<"URL2">>], 366: <<"desc">> => [<<"DescTest">>, <<"DESC2">>], 367: <<"class">> => [ 368: #{<<"tags">> => [<<"CONFIDENTIAL">>, <<"PRIVATE">>]}, 369: #{<<"tags">> => [<<"CONFIDENTIAL">>]} 370: ], 371: <<"key">> => [ 372: #{<<"type">> => <<"TYPETEST1">>, <<"credential">> => <<"TESTCREDENTIAL1">>}, 373: #{<<"type">> => <<"TYPETEST2">>, <<"credential">> => <<"TESTCREDENTIAL2">>} 374: ] 375: }. 376: 377: agent_vcard_input() -> 378: #{<<"formattedName">> => <<"TestName">>, 379: <<"nameComponents">> => #{ 380: <<"family">> => <<"familyName">>, 381: <<"givenName">> => <<"givenName">>, 382: <<"middleName">> => <<"middleName">>, 383: <<"prefix">> => <<"prefix">>, 384: <<"suffix">> => <<"sufix">> 385: }, 386: <<"nickname">> => [<<"NicknameTest">>, <<"SecondNickname">>], 387: <<"photo">> => [ 388: #{<<"type">> => <<"image/jpeg">>, 389: <<"binValue">> => <<"TestBinaries">>}, 390: #{<<"extValue">> => <<"External Value">>} 391: ]}. 392: 393: admin_get_address_query() -> 394: <<"query Q1($user: JID!) 395: { 396: vcard 397: { 398: getVcard(user: $user) 399: { 400: address 401: { 402: tags 403: pobox 404: extadd 405: street 406: locality 407: region 408: pcode 409: country 410: } 411: } 412: } 413: }">>. 414: 415: user_get_query() -> 416: <<"query Q1($user: JID) 417: { 418: vcard 419: { 420: getVcard(user: $user) 421: { 422: formattedName 423: email 424: { 425: userId 426: tags 427: } 428: } 429: } 430: }">>. 431: 432: admin_get_full_vcard_as_result_mutation() -> 433: ResultFormat = get_full_vcard_as_result(), 434: <<"mutation M1($vcard: VcardInput!, $user: JID!)", 435: "{vcard{setVcard(vcard: $vcard, user: $user)", ResultFormat/binary, "}}">>. 436: 437: user_get_full_vcard_as_result_mutation() -> 438: ResultFormat = get_full_vcard_as_result(), 439: <<"mutation M1($vcard: VcardInput!){vcard{setVcard(vcard: $vcard)", 440: ResultFormat/binary, "}}">>. 441: 442: user_get_full_vcard_as_result_query() -> 443: ResultFormat = get_full_vcard_as_result(), 444: <<"query Q1($user: JID){vcard{getVcard(user: $user)", 445: ResultFormat/binary, "}}">>. 446: 447: admin_get_full_vcard_as_result_query() -> 448: ResultFormat = get_full_vcard_as_result(), 449: <<"query Q1($user: JID!)", 450: "{vcard{getVcard(user: $user)", ResultFormat/binary, "}}">>. 451: 452: get_full_vcard_as_result() -> 453: <<"{ 454: formattedName 455: nameComponents 456: { family givenName middleName prefix suffix } 457: nickname 458: photo 459: { 460: ... on ImageData 461: { type binValue } 462: ... on External 463: { extValue } 464: } 465: birthday 466: address 467: { tags pobox extadd street locality region pcode country } 468: label 469: { tags line } 470: telephone 471: { tags number } 472: email 473: { tags userId } 474: jabberId 475: mailer 476: timeZone 477: geo 478: { lat lon } 479: title 480: role 481: logo 482: { 483: ... on ImageData 484: { type binValue } 485: ... on External 486: { extValue } 487: } 488: agent 489: { 490: ... on External 491: { extValue } 492: ... on AgentVcard 493: { 494: vcard 495: { 496: formattedName 497: nameComponents 498: { family givenName middleName prefix suffix } 499: nickname 500: photo 501: { 502: ... on ImageData 503: { type binValue } 504: ... on External 505: { extValue } 506: } 507: } 508: } 509: } 510: org 511: { orgname orgunit } 512: categories 513: { keyword } 514: note 515: prodId 516: rev 517: sortString 518: sound 519: { 520: ... on External 521: { extValue } 522: ... on BinValue 523: { binValue } 524: ... on Phonetic 525: { phonetic } 526: } 527: uid 528: url 529: desc 530: class 531: { tags } 532: key 533: { type credential } 534: }">>.