├── app.yaml ├── .gitignore ├── .procfile ├── src ├── dataScraping │ ├── index.js │ ├── restCountries.js │ └── emojiFlags.json ├── utils │ └── async.js ├── neo4j │ └── index.js └── graphql │ ├── getInferedSchema.js │ ├── index.js │ ├── schemaInfered.graphql │ └── schema.graphql ├── .babelrc ├── .github └── FUNDING.yml ├── apollo.config.js ├── blog ├── images │ ├── banner.ai │ ├── banner.png │ ├── draw-io-example.png │ ├── arabic-official-language.png │ ├── top-10-official-languages.png │ ├── portuguese-official-language.png │ └── bordering-countries-all-shortest-path.png └── How to transform a rest service to a graph service.md ├── examples ├── react-apollo │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── src │ │ ├── setupTests.js │ │ ├── App.test.js │ │ ├── index.js │ │ ├── App.js │ │ └── serviceWorker.js │ ├── .gitignore │ ├── README.md │ └── package.json └── vanilla-js │ ├── README.md │ └── index.html ├── .gcloudignore ├── scratchBook.cypher ├── package.json ├── graphql-voyager └── index.html └── README.md /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs10 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | build -------------------------------------------------------------------------------- /.procfile: -------------------------------------------------------------------------------- 1 | web: node ./build/graphql/index.js 2 | -------------------------------------------------------------------------------- /src/dataScraping/index.js: -------------------------------------------------------------------------------- 1 | import "./restCountries"; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: lennertVanSever 4 | -------------------------------------------------------------------------------- /apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | service: { 3 | name: "graphql-countries" 4 | } 5 | } -------------------------------------------------------------------------------- /blog/images/banner.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/blog/images/banner.ai -------------------------------------------------------------------------------- /blog/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/blog/images/banner.png -------------------------------------------------------------------------------- /examples/react-apollo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /blog/images/draw-io-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/blog/images/draw-io-example.png -------------------------------------------------------------------------------- /blog/images/arabic-official-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/blog/images/arabic-official-language.png -------------------------------------------------------------------------------- /blog/images/top-10-official-languages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/blog/images/top-10-official-languages.png -------------------------------------------------------------------------------- /examples/react-apollo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/examples/react-apollo/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-apollo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/examples/react-apollo/public/logo192.png -------------------------------------------------------------------------------- /examples/react-apollo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/examples/react-apollo/public/logo512.png -------------------------------------------------------------------------------- /blog/images/portuguese-official-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/blog/images/portuguese-official-language.png -------------------------------------------------------------------------------- /blog/images/bordering-countries-all-shortest-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennertVanSever/graphcountries/HEAD/blog/images/bordering-countries-all-shortest-path.png -------------------------------------------------------------------------------- /examples/vanilla-js/README.md: -------------------------------------------------------------------------------- 1 | A simple example on how to use the GraphQL API with Vanilla Javascript. You can find a [live version here](https://graphcountries-example-vanilla-js.netlify.app). 2 | -------------------------------------------------------------------------------- /src/utils/async.js: -------------------------------------------------------------------------------- 1 | export const asyncForEach = async (array, callback) => { 2 | for (let index = 0; index < array.length; index++) { 3 | await callback(array[index], index, array); 4 | } 5 | } -------------------------------------------------------------------------------- /examples/react-apollo/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/neo4j/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | import neo4j from 'neo4j-driver'; 3 | 4 | const { 5 | BOLT_ADDRESS, 6 | DB_USERNAME, 7 | DB_PASSWORD 8 | } = process.env; 9 | 10 | export const driver = neo4j.driver(BOLT_ADDRESS, neo4j.auth.basic(DB_USERNAME, DB_PASSWORD)); 11 | export const session = driver.session(); -------------------------------------------------------------------------------- /examples/react-apollo/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/graphql/getInferedSchema.js: -------------------------------------------------------------------------------- 1 | import { inferSchema } from 'neo4j-graphql-js'; 2 | import { driver } from '../neo4j/index.js'; 3 | import fs from 'fs'; 4 | 5 | export const inferNeo4jSchema = async () => { 6 | const { typeDefs } = await inferSchema(driver, { alwaysIncludeRelationships: false }); 7 | fs.writeFileSync('./src/graphql/schemaInfered.graphql', typeDefs); 8 | } -------------------------------------------------------------------------------- /examples/react-apollo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/react-apollo/README.md: -------------------------------------------------------------------------------- 1 | A simple example on how to use the GraphQL API with Create React App, the two interesting files are the [index.js](src/index.js) and [app.js](src/App.js) files. You can also see a [live version here](https://graphcountries-example-cra.netlify.app). 2 | 3 | ### `yarn start` 4 | 5 | Runs the app in the development mode.
6 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 7 | 8 | The page will reload if you make edits.
9 | You will also see any lint errors in the console. 10 | 11 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /examples/react-apollo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /scratchBook.cypher: -------------------------------------------------------------------------------- 1 | // get the graph diameter 2 | MATCH (a:Country), (b:Country) WHERE id(a) > id(b) 3 | MATCH p=shortestPath((a)-[:borders*]-(b)) 4 | WITH length(p) AS len, p 5 | ORDER BY len DESC LIMIT 1 6 | RETURN p 7 | 8 | // countries with most borders 9 | MATCH (country:Country) 10 | WITH country, SIZE(()-[:borders]-(country)) as borderCount 11 | ORDER BY borderCount DESC 12 | RETURN country.name, borderCount 13 | 14 | // languages ordered by prevelance in countries 15 | MATCH (country:Country) 16 | MATCH (language:Language) 17 | WITH country, language, SIZE(()-[:hasOfficialLanguage]-(language)) as countryCount 18 | ORDER BY countryCount DESC 19 | RETURN DISTINCT(language.name), countryCount -------------------------------------------------------------------------------- /examples/react-apollo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import { ApolloProvider } from '@apollo/react-hooks'; 6 | import ApolloClient from 'apollo-boost'; 7 | 8 | const client = new ApolloClient({ 9 | uri: 'https://countries-274616.ew.r.appspot.com', 10 | }); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ); 20 | 21 | // If you want your app to work offline and load faster, you can change 22 | // unregister() to register() below. Note this comes with some pitfalls. 23 | // Learn more about service workers: https://bit.ly/CRA-PWA 24 | serviceWorker.unregister(); 25 | -------------------------------------------------------------------------------- /examples/react-apollo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-apollo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "react": "^16.13.1", 10 | "react-dom": "^16.13.1", 11 | "react-scripts": "3.4.1" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test", 17 | "eject": "react-scripts eject" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/react-apollo/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useQuery } from '@apollo/react-hooks'; 3 | import { gql } from 'apollo-boost'; 4 | 5 | const REGIONS = gql` 6 | { 7 | Region(orderBy: name_asc) { 8 | name 9 | children: subregions(orderBy: name_asc) { 10 | name 11 | children: countries(orderBy: name_asc) { 12 | name 13 | } 14 | } 15 | } 16 | } 17 | `; 18 | 19 | const App = () => { 20 | const { loading, error, data } = useQuery(REGIONS); 21 | if (loading) return

Loading...

; 22 | if (error) return

Error :(

; 23 | const renderList = (list) => ( 24 | 32 | ) 33 | return renderList(data.Region); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /src/graphql/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | import { makeAugmentedSchema, inferSchema } from 'neo4j-graphql-js'; 3 | import { ApolloServer } from 'apollo-server'; 4 | import { driver } from '../neo4j/index.js'; 5 | import fs from 'fs'; 6 | 7 | 8 | fs.readFile('./src/graphql/schema.graphql', (error, result) => { 9 | if (error) { 10 | console.error(error); 11 | } else { 12 | const typeDefs = result.toString(); 13 | const generatedSchema = makeAugmentedSchema({ 14 | typeDefs, 15 | config: { 16 | query: { 17 | exclude: ["AlternativeSpelling", "Area", "OtherAcronym", "OtherName"] 18 | }, 19 | mutation: false 20 | } 21 | }); 22 | 23 | const server = new ApolloServer({ 24 | schema: generatedSchema, 25 | context: ({ req }) => { 26 | return { 27 | driver, 28 | req 29 | }; 30 | }, 31 | engine: { 32 | apiKey: "service:graphql-countries:MHaMUmJGfYSddSP_IU5Iqg", 33 | }, 34 | introspection: true, 35 | playground: true, 36 | }); 37 | const port = process.env.PORT || 8080; 38 | 39 | server.listen(port, '0.0.0.0').then(({ url }) => { 40 | console.log(`GraphQL API ready at ${url}`); 41 | }).catch(err => console.error(err)); 42 | } 43 | }) -------------------------------------------------------------------------------- /examples/vanilla-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vanilla js example graph countries 8 | 9 | 10 | 11 | 12 | 13 |
14 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "countries", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf build && mkdir build", 8 | "build-babel": "babel -d ./build ./src -s", 9 | "build": "npm run clean && npm run build-babel", 10 | "deploy": "npm run build && gcloud app deploy --quiet", 11 | "logs": "gcloud app logs read", 12 | "start": "node ./build/graphql/index.js", 13 | "dev": "npm run build && npm start", 14 | "dataScraping": "babel-node -- ./src/dataScraping/index.js", 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "@apollo/react-hooks": "^3.1.5", 22 | "@babel/cli": "^7.8.4", 23 | "@babel/core": "^7.9.0", 24 | "@babel/node": "^7.8.7", 25 | "@babel/preset-env": "^7.9.5", 26 | "apollo-boost": "^0.4.9", 27 | "apollo-server": "^2.12.0", 28 | "cheerio": "^1.0.0-rc.3", 29 | "cheerio-tableparser": "^1.0.1", 30 | "dotenv": "^8.2.0", 31 | "graphql": "^15.0.0", 32 | "graphql-import": "^1.0.2", 33 | "graphql-tag": "^2.10.3", 34 | "html2json": "^1.0.2", 35 | "neo4j-driver": "4.0.2", 36 | "neo4j-graphql-js": "^2.13.0", 37 | "node-fetch": "^2.6.0", 38 | "tabletojson": "^2.0.4" 39 | }, 40 | "devDependencies": { 41 | "nodemon": "^2.0.3" 42 | }, 43 | "nodemonConfig": { 44 | "watch": [ 45 | "graphql/*" 46 | ], 47 | "ext": "js, graphql", 48 | "delay": 1000 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/react-apollo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /graphql-voyager/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 |
Loading...
38 | 70 | 71 | -------------------------------------------------------------------------------- /src/graphql/schemaInfered.graphql: -------------------------------------------------------------------------------- 1 | type Country { 2 | _id: Long! 3 | alpha2Code: String! 4 | alpha3Code: String! 5 | area: Float 6 | capital: String! 7 | demonym: String! 8 | gini: Float 9 | location: Point! 10 | name: String! 11 | nativeName: String! 12 | numericCode: String 13 | population: Float! 14 | hasofficiallanguage: [Language] @relation(name: "hasOfficialLanguage", direction: "OUT") 15 | hastopleveldomain: [TopLevelDomain] @relation(name: "hasTopLevelDomain", direction: "OUT") 16 | hascallingcode: [CallingCode] @relation(name: "hasCallingCode", direction: "OUT") 17 | hasalternativespelling: [AlternativeSpelling] @relation(name: "hasAlternativeSpelling", direction: "OUT") 18 | hastimezone: [Timezone] @relation(name: "hasTimezone", direction: "OUT") 19 | hassubregion: [Subregion] @relation(name: "hasSubregion", direction: "OUT") 20 | haslocale: [Translation] @relation(name: "hasLocale", direction: "OUT") 21 | hasflag: [Flag] @relation(name: "hasFlag", direction: "OUT") 22 | hascurrency: [Currency] @relation(name: "hasCurrency", direction: "OUT") 23 | hasregionalbloc: [RegionalBloc] @relation(name: "hasRegionalBloc", direction: "OUT") 24 | borders: [Country] @relation(name: "borders", direction: "OUT") 25 | } 26 | 27 | type Language { 28 | _id: Long! 29 | iso639_1: String! 30 | iso639_2: String! 31 | name: String! 32 | nativeName: String! 33 | countrys: [Country] @relation(name: "hasOfficialLanguage", direction: "IN") 34 | } 35 | 36 | type TopLevelDomain { 37 | _id: Long! 38 | name: String! 39 | countrys: [Country] @relation(name: "hasTopLevelDomain", direction: "IN") 40 | } 41 | 42 | type CallingCode { 43 | _id: Long! 44 | name: String! 45 | countrys: [Country] @relation(name: "hasCallingCode", direction: "IN") 46 | } 47 | 48 | type AlternativeSpelling { 49 | _id: Long! 50 | name: String! 51 | countrys: [Country] @relation(name: "hasAlternativeSpelling", direction: "IN") 52 | } 53 | 54 | type Timezone { 55 | _id: Long! 56 | name: String! 57 | countrys: [Country] @relation(name: "hasTimezone", direction: "IN") 58 | } 59 | 60 | type Subregion { 61 | _id: Long! 62 | name: String! 63 | hasregion: [Region] @relation(name: "hasRegion", direction: "OUT") 64 | countrys: [Country] @relation(name: "hasSubregion", direction: "IN") 65 | } 66 | 67 | type Region { 68 | _id: Long! 69 | name: String! 70 | subregions: [Subregion] @relation(name: "hasRegion", direction: "IN") 71 | } 72 | 73 | type Translation { 74 | _id: Long! 75 | languageCode: String! 76 | value: String! 77 | countrys: [Country] @relation(name: "hasLocale", direction: "IN") 78 | } 79 | 80 | type Flag { 81 | _id: Long! 82 | emoji: String! 83 | emojiUnicode: String! 84 | svgFile: String! 85 | countrys: [Country] @relation(name: "hasFlag", direction: "IN") 86 | } 87 | 88 | type Currency { 89 | _id: Long! 90 | code: String! 91 | name: String! 92 | symbol: String! 93 | countrys: [Country] @relation(name: "hasCurrency", direction: "IN") 94 | } 95 | 96 | type RegionalBloc { 97 | _id: Long! 98 | acronym: String! 99 | name: String! 100 | hasothername: [OtherName] @relation(name: "hasOtherName", direction: "OUT") 101 | hasotheracronym: [OtherAcronym] @relation(name: "hasOtherAcronym", direction: "OUT") 102 | countrys: [Country] @relation(name: "hasRegionalBloc", direction: "IN") 103 | } 104 | 105 | type OtherName { 106 | _id: Long! 107 | name: String! 108 | regionalblocs: [RegionalBloc] @relation(name: "hasOtherName", direction: "IN") 109 | } 110 | 111 | type OtherAcronym { 112 | _id: Long! 113 | name: String! 114 | regionalblocs: [RegionalBloc] @relation(name: "hasOtherAcronym", direction: "IN") 115 | } 116 | 117 | type Unit { 118 | _id: Long! 119 | name: String! 120 | unit: String! 121 | value: String! 122 | } 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /examples/react-apollo/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | enum AreaUnit { 2 | SQUARE_KILOMETERS 3 | SQUARE_METERS 4 | SQUARE_MILES 5 | } 6 | 7 | type Country { 8 | _id: String! 9 | 10 | "ISO 3166-1 alpha-2 codes are two-letter country codes defined in ISO 3166-1, part of the ISO 3166 standard published by the International Organization for Standardization (ISO), to represent countries, dependent territories, and special areas of geographical interest. https://en.m.wikipedia.org/wiki/ISO_3166-1_alpha-2" 11 | alpha2Code: String! 12 | 13 | "ISO 3166-1 alpha-3 codes are three-letter country codes defined in ISO 3166-1, part of the ISO 3166 standard published by the International Organization for Standardization (ISO), to represent countries, dependent territories, and special areas of geographical interest. https://en.m.wikipedia.org/wiki/ISO_3166-1_alpha-3" 14 | alpha3Code: String! 15 | 16 | "The area in square kilometer, you can convert the area unit and population density through the convertedArea property" 17 | area: Float 18 | 19 | capital: String! 20 | 21 | "The population per square kilometer" 22 | populationDensity: Float 23 | @cypher( 24 | statement: """ 25 | return this.population / this.area 26 | """ 27 | ) 28 | 29 | 30 | convertedArea(areaUnit: AreaUnit = SQUARE_MILES): Area @relation(name: "NOTHING", direction: "IN") 31 | @cypher( 32 | statement: """ 33 | MATCH (unit:Unit { name: areaUnit }) 34 | return { 35 | value: unit.value * this.area, 36 | populationDensity: this.population / (unit.value * this.area), 37 | unit: unit.unit 38 | } 39 | """ 40 | ) 41 | 42 | "identifies residents or natives of a particular place, usually derived from the name of the place or that of an ethnic group. https://en.m.wikipedia.org/wiki/Demonym" 43 | demonym: String! 44 | 45 | "In economics, the Gini coefficient, sometimes called the Gini index or Gini ratio, is a measure of statistical dispersion intended to represent the income or wealth distribution of a nation's residents, and is the most commonly used measurement of inequality. https://en.m.wikipedia.org/wiki/Gini_coefficient" 46 | gini: Float 47 | 48 | location: Point! 49 | name: String! 50 | nameTranslations: [Translation] @relation(name: "hasLocale", direction: "OUT") 51 | nativeName: String! 52 | 53 | numericCode: String 54 | population: Float! 55 | 56 | "A country code top-level domain (ccTLD) is an Internet top-level domain generally used or reserved for a country, sovereign state, or dependent territory identified with a country code. https://en.m.wikipedia.org/wiki/Country_code_top-level_domain" 57 | topLevelDomains: [TopLevelDomain] @relation(name: "hasTopLevelDomain", direction: "OUT") 58 | 59 | callingCodes: [CallingCode] @relation(name: "hasCallingCode", direction: "OUT") 60 | alternativeSpellings: [AlternativeSpelling] @relation(name: "hasAlternativeSpelling", direction: "OUT") 61 | timezones: [Timezone] @relation(name: "hasTimezone", direction: "OUT") 62 | 63 | borders: [Country] @relation(name: "borders", direction: "IN") 64 | @cypher( 65 | statement: """ 66 | MATCH(country:Country) 67 | MATCH(this)-[r:borders]-(country) 68 | return country 69 | """ 70 | ) 71 | 72 | subregion: Subregion @relation(name: "hasSubregion", direction: "OUT") 73 | # region: Region @relation(name: "hasRegion", direction: "OUT") 74 | officialLanguages: [Language] @relation(name: "hasOfficialLanguage", direction: "OUT") 75 | currencies: [Currency] @relation(name: "hasCurrency", direction: "OUT") 76 | regionalBlocs: [RegionalBloc] @relation(name: "hasRegionalBloc", direction: "OUT") 77 | flag: Flag @relation(name: "hasFlag", direction: "OUT") 78 | 79 | distanceToOtherCountries: [DistanceToOtherCountry] @relation(name: "NOTHING", direction: "IN") 80 | @cypher( 81 | statement: """ 82 | MATCH(country:Country) 83 | WHERE NOT this = country 84 | WITH (distance(this.location, country.location) / 1000) as distanceInKm, country 85 | ORDER BY distanceInKm 86 | RETURN { distanceInKm: distanceInKm, countryName: country.name } 87 | """ 88 | ) 89 | 90 | 91 | 92 | "Shortest path to go from one country to another with the least border crossing as possible" 93 | shortestPathToOtherCountry(otherCountryAlpha2Code: String!): [Country] @relation(name: "borders", direction: "OUT") 94 | @cypher( 95 | statement: """ 96 | MATCH (to:Country {alpha2Code: $otherCountryAlpha2Code}) 97 | WHERE NOT this = to 98 | MATCH p = shortestPath((this)-[:borders*]-(to)) 99 | UNWIND nodes(p) as n 100 | RETURN distinct n 101 | """ 102 | ) 103 | } 104 | 105 | 106 | type Timezone { 107 | _id: String! 108 | name: String! 109 | countries: [Country] @relation(name: "hasTimezone", direction: "IN") 110 | } 111 | 112 | type Subregion { 113 | _id: String! 114 | name: String! 115 | region: Region @relation(name: "hasRegion", direction: "OUT") 116 | countries: [Country] @relation(name: "hasSubregion", direction: "IN") 117 | } 118 | 119 | type Region { 120 | _id: String! 121 | name: String! 122 | subregions: [Subregion] @relation(name: "hasRegion", direction: "IN") 123 | } 124 | 125 | type Language { 126 | _id: String! 127 | iso639_1: String! 128 | iso639_2: String! 129 | name: String! 130 | nativeName: String! 131 | countries: [Country] @relation(name: "hasOfficialLanguage", direction: "IN") 132 | } 133 | 134 | type Currency { 135 | _id: String! 136 | code: String! 137 | name: String! 138 | symbol: String! 139 | countries: [Country] @relation(name: "hasCurrency", direction: "IN") 140 | } 141 | 142 | type RegionalBloc { 143 | _id: String! 144 | acronym: String! 145 | name: String! 146 | otherAcronyms: [OtherAcronym] @relation(name: "hasOtherAcronym", direction: "OUT") 147 | otherNames: [OtherName] @relation(name: "hasOtherName", direction: "OUT") 148 | countries: [Country] @relation(name: "hasRegionalBlocs", direction: "IN") 149 | } 150 | 151 | type OtherAcronym { 152 | _id: String! 153 | name: String! 154 | regionalBlocs: [RegionalBloc] @relation(name: "hasOtherAcronym", direction: "IN") 155 | } 156 | 157 | type OtherName { 158 | _id: String! 159 | name: String! 160 | regionalBlocs: [RegionalBloc] @relation(name: "hasOtherName", direction: "IN") 161 | } 162 | 163 | type Area { 164 | value: Float 165 | unit: String 166 | "The population per square " 167 | populationDensity: Float 168 | } 169 | 170 | type Translation { 171 | _id: String! 172 | languageCode: String! 173 | value: String! 174 | } 175 | 176 | type Flag { 177 | _id: String! 178 | emoji: String! 179 | emojiUnicode: String! 180 | svgFile: String! 181 | country: Country @relation(name: "hasFlag", direction: "IN") 182 | } 183 | 184 | type DistanceToOtherCountry { 185 | distanceInKm: Float 186 | "Sorry can't return the full country type :/" 187 | countryName: String 188 | } 189 | 190 | type TopLevelDomain { 191 | _id: String! 192 | name: String! 193 | countries: [Country] @relation(name: "hasTopLevelDomain", direction: "IN") 194 | } 195 | 196 | type CallingCode { 197 | _id: String! 198 | name: String! 199 | countries: [Country] @relation(name: "hasCallingCode", direction: "IN") 200 | } 201 | 202 | type AlternativeSpelling { 203 | _id: String! 204 | name: String! 205 | countries: [Country] @relation(name: "hasAlternativeSpelling", direction: "IN") 206 | } 207 | -------------------------------------------------------------------------------- /src/dataScraping/restCountries.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { driver, session } from '../neo4j/index.js'; 3 | import { asyncForEach } from '../utils/async'; 4 | import { inferNeo4jSchema } from '../graphql/getInferedSchema'; 5 | import emojiFlags from './emojiFlags.json'; 6 | 7 | const resetDatabase = async () => { 8 | await session.run(` 9 | match (a) -[r] -> () delete a, r 10 | `); 11 | await session.run (` 12 | match (a) delete a 13 | `) 14 | } 15 | 16 | const getData = async (callback) => { 17 | 18 | const responseCountries = await fetch('https://restcountries.eu/rest/v2/all'); 19 | const countries = await responseCountries.json(); 20 | 21 | await asyncForEach(countries, async (country) => { 22 | await callback(country, { svgFile: country.flag, ...emojiFlags[country.alpha2Code] }); 23 | }); 24 | } 25 | 26 | const setCountry = async (country, flag) => { 27 | await session.run( 28 | ` 29 | CREATE (country:Country { 30 | name: $name, 31 | alpha2Code: $alpha2Code, 32 | alpha3Code: $alpha3Code, 33 | population: $population, 34 | demonym: $demonym, 35 | capital: $capital, 36 | area: $area, 37 | gini: $gini, 38 | nativeName: $nativeName, 39 | numericCode: $numericCode, 40 | location: Point({latitude: $latitude, longitude: $longitude}) 41 | }) 42 | return ID(country) 43 | `, { 44 | ...country, 45 | latitude: country.latlng[0] || 0, 46 | longitude: country.latlng[1] || 0, 47 | } 48 | ); 49 | console.log(country.name); 50 | await createSimpleNodesAndLinkToIt('TopLevelDomain', 'hasTopLevelDomain', country.topLevelDomain, country.name); 51 | await createSimpleNodesAndLinkToIt('CallingCode', 'hasCallingCode', country.callingCodes, country.name); 52 | await createSimpleNodesAndLinkToIt('AlternativeSpelling', 'hasAlternativeSpelling', country.altSpellings, country.name); 53 | await createSimpleNodesAndLinkToIt('Timezone', 'hasTimezone', country.timezones, country.name); 54 | await createBorderRelationShip(country); 55 | await createRegions(country); 56 | await createTranslatedCountryNames(country); 57 | await createComplexNodesAndLinkToIt('Flag', 'hasFlag', [flag], country.name); 58 | await createComplexNodesAndLinkToIt('Language', 'hasOfficialLanguage', country.languages, country.name) 59 | await createComplexNodesAndLinkToIt('Currency', 'hasCurrency', country.currencies, country.name) 60 | await createComplexNodesAndLinkToIt('Currency', 'hasCurrency', country.currencies, country.name) 61 | await createRegionalBloc(country); 62 | } 63 | 64 | const createSimpleNodesAndLinkToIt = async (newNodeType, relationType, newNodeNames, countryName) => { 65 | await asyncForEach(newNodeNames, async (newNodeName) => { 66 | if (newNodeName) { 67 | const newNode = await session.run( 68 | ` 69 | MATCH (country:Country { name: $countryName }) 70 | MERGE (n:${newNodeType} { name: $newNodeName }) 71 | MERGE (country)-[r:${relationType}]->(n) 72 | return ID(n)`, 73 | { 74 | newNodeName, 75 | countryName, 76 | } 77 | ); 78 | } 79 | // console.log(`inserted ${newNodeType}`, newNodeName); 80 | }); 81 | } 82 | 83 | 84 | const createComplexNodesAndLinkToIt = async (newNodeType, relationType, newNodesData, countryName) => { 85 | await asyncForEach(newNodesData, async (newNodeData) => { 86 | let inputNewNode = `{ `; 87 | Object.keys(newNodeData).forEach((key, index) => { 88 | const value = newNodeData[key]; 89 | inputNewNode += `${key}: "${newNodeData[key]}"`; 90 | if (index < Object.keys(newNodeData).length - 1) inputNewNode += ', '; 91 | }); 92 | inputNewNode += ' }'; 93 | const newNode = await session.run( 94 | ` 95 | MATCH (country:Country { name: $countryName }) 96 | MERGE (n:${newNodeType} ${inputNewNode}) 97 | MERGE (country)-[r:${relationType}]->(n) 98 | return ID(n)`, 99 | { 100 | countryName, 101 | } 102 | ); 103 | // console.log(`inserted ${newNodeType}`, newNodeData); 104 | }); 105 | } 106 | 107 | const createRegionalBloc = async (country) => { 108 | await asyncForEach(country.regionalBlocs, async (regionalBloc) => { 109 | await session.run( 110 | ` 111 | MATCH (country:Country { name: $countryName }) 112 | MERGE (regionalBloc:RegionalBloc { acronym: $acronym, name: $regionalBlocName }) 113 | MERGE (country)-[r:hasRegionalBloc]->(regionalBloc) 114 | `, 115 | { 116 | countryName: country.name, 117 | regionalBlocName: regionalBloc.name, 118 | acronym: regionalBloc.acronym, 119 | } 120 | ); 121 | await asyncForEach(regionalBloc.otherAcronyms, async otherAcronym => { 122 | await session.run( 123 | ` 124 | MATCH (regionalBloc:RegionalBloc { name: $regionalBlocName }) 125 | MERGE (otherAcronym:OtherAcronym { name: $otherAcronym }) 126 | MERGE (regionalBloc)-[r:hasOtherAcronym]->(otherAcronym) 127 | `, 128 | { 129 | regionalBlocName: regionalBloc.name, 130 | otherAcronym 131 | } 132 | ); 133 | }); 134 | await asyncForEach(regionalBloc.otherNames, async otherName => { 135 | await session.run( 136 | ` 137 | MATCH (regionalBloc:RegionalBloc { name: $regionalBlocName }) 138 | MERGE (otherName:OtherName { name: $otherName }) 139 | MERGE (regionalBloc)-[r:hasOtherName]->(otherName) 140 | `, 141 | { 142 | regionalBlocName: regionalBloc.name, 143 | otherName 144 | } 145 | ); 146 | }); 147 | }); 148 | } 149 | 150 | const createBorderRelationShip = async ({ alpha3Code, borders }) => { 151 | await asyncForEach(borders, async (borderCode) => { 152 | try { 153 | await session.run( 154 | ` 155 | MATCH(a: Country { alpha3Code: $code1 }) 156 | MATCH(b: Country { alpha3Code: $code2 }) 157 | MERGE(a)-[:borders]-(b) 158 | return a, b 159 | `, { 160 | code1: alpha3Code, 161 | code2: borderCode, 162 | } 163 | ) 164 | // console.log('insert border', alpha3Code, borderCode); 165 | } 166 | catch(e) { 167 | console.log('insert border fail'); 168 | } 169 | }) 170 | } 171 | 172 | const createRegions = async ({ name, region, subregion }) => { 173 | if (region) { 174 | try { 175 | await session.run( 176 | ` 177 | MATCH(country: Country { name: $name }) 178 | MERGE(subregion: Subregion { name: $subregion }) 179 | MERGE(region: Region { name: $region }) 180 | MERGE(country)-[:hasSubregion]->(subregion) 181 | // MERGE(country)-[:hasRegion]->(region) 182 | MERGE(subregion)-[:hasRegion]->(region) 183 | `, { 184 | name, region, subregion 185 | } 186 | ) 187 | } 188 | catch(e) { 189 | console.log(e); 190 | } 191 | } 192 | } 193 | 194 | const makeUnits = async () => { 195 | await session.run(` 196 | CREATE(n1:Unit { name: 'SQUARE_METERS', value: 1000000, unit: 'm2' }) 197 | CREATE(n2:Unit { name: 'SQUARE_KILOMETERS', value: 1, unit: 'km2' }) 198 | CREATE(n3:Unit { name: 'SQUARE_MILES', value: 0.386102, unit: 'mi2' }) 199 | `) 200 | } 201 | 202 | const createTranslatedCountryNames = async ({ translations, name }) => { 203 | await asyncForEach(Object.keys(translations), async languageCode => { 204 | const translationValue = translations[languageCode]; 205 | if (translationValue) { 206 | await session.run( 207 | ` 208 | MATCH(country: Country { name: $name }) 209 | MERGE(translation: Translation { value: $translationValue, languageCode: $languageCode }) 210 | MERGE(country)-[:hasLocale]->(translation) 211 | `, { 212 | name, 213 | languageCode, 214 | translationValue 215 | } 216 | ) 217 | } 218 | }); 219 | } 220 | 221 | (async () => { 222 | await resetDatabase(); 223 | await makeUnits(); 224 | await getData(setCountry); 225 | await inferNeo4jSchema(); 226 | driver.close() 227 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graph countries 2 | Use GraphQL to query country-related data, free of charge and without restrictions. The data is the same as [restcountries.eu](https://restcountries.eu/) with extra emoji's for [flags](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%20%20Flag%20%7B%0A%20%20%20%20emoji%0A%20%20%20%20country%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) because who doesn't like emoji's? 3 | 4 | 5 | ### [Explore the playground](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%20%7B%0A%20%20%20%20name%0A%20%20%20%20%23%20check%20the%20docs%20for%20more%20info%0A%20%20%7D%0A%7D%0A) 6 | 7 | 8 | Some example queries: 9 | 10 | * [Countries](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%20%20Country%20%7B%0A%20%20%20%20name%0A%20%20%20%20nativeName%0A%20%20%20%20alpha2Code%0A%20%20%20%20alpha3Code%0A%20%20%20%20area%0A%20%20%20%20population%0A%20%20%20%20populationDensity%0A%20%20%20%20capital%0A%20%20%20%20demonym%0A%20%20%20%20gini%0A%20%20%20%20location%20%7B%0A%20%20%20%20%20%20latitude%0A%20%20%20%20%20%20longitude%0A%20%20%20%20%7D%0A%20%20%20%20numericCode%0A%20%20%20%20subregion%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20region%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20officialLanguages%20%7B%0A%20%20%20%20%20%20iso639_1%0A%20%20%20%20%20%20iso639_2%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20nativeName%0A%20%20%20%20%7D%0A%20%20%20%20currencies%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20symbol%0A%20%20%20%20%7D%0A%20%20%20%20regionalBlocs%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20acronym%0A%20%20%20%20%20%20otherAcronyms%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20otherNames%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20flag%20%7B%0A%20%20%20%20%20%20emoji%0A%20%20%20%20%20%20emojiUnicode%0A%20%20%20%20%20%20svgFile%0A%20%20%20%20%7D%0A%20%20%20%20topLevelDomains%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20callingCodes%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20alternativeSpellings%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 11 | * [Timezones](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%20%20Timezone%28orderBy%3A%20name_asc%29%20%7B%0A%20%20%20%20name%0A%20%20%20%20countries%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 12 | * [Currencies](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%20%20Currency%20%7B%0A%20%20%20%20name%0A%20%20%20%20code%0A%20%20%20%20symbol%0A%20%20%20%20countries%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 13 | * [Languages](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Language%20%7B%0A%20%20%20%20iso639_1%0A%20%20%20%20iso639_2%0A%20%20%20%20name%0A%20%20%20%20nativeName%0A%20%20%20%20countries%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 14 | * [Regions + subregions](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Region%20%7B%0A%20%20%20%20subregions%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20countries%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 15 | * [Top level domains](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09TopLevelDomain%20%7B%0A%20%20%20%20name%0A%20%20%20%20countries%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 16 | * [Calling codes](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09CallingCode%20%7B%0A%20%20%20%20name%0A%20%20%20%20countries%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 17 | * [Flags](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%20%20Flag%20%7B%0A%20%20%20%20emoji%0A%20%20%20%20svgFile%0A%20%20%20%20emojiUnicode%0A%20%20%20%20country%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 18 | * [Bordering countries](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%20%7B%0A%20%20%20%20name%0A%20%20%20%20borders%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 19 | * [Translations](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%20%7B%0A%20%20%20%20name%0A%20%20%20%20nameTranslations%28filter%3A%20%7B%20languageCode_in%3A%20%5B%22fr%22%2C%20%22nl%22%5D%7D%29%20%7B%0A%20%20%20%20%20%20value%0A%20%20%20%20%20%20languageCode%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 20 | * [Shortest path to another country](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%20%7B%0A%20%20%20%20name%0A%20%20%20%20shortestPathToOtherCountry%28otherCountryAlpha2Code%3A%20%22RU%22%29%20%7B%0A%09%09%09name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 21 | * [Distance to other countries](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%20%7B%0A%20%20%20%20name%0A%20%20%20%20distanceToOtherCountries%28first%3A%205%29%20%7B%0A%20%20%20%20%20%20distanceInKm%0A%20%20%20%20%20%20countryName%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 22 | * [A combination of everything](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%28alpha2Code%3A%20%22BE%22%29%20%7B%0A%20%20%20%20name%0A%20%20%20%20nativeName%0A%20%20%20%20alpha2Code%0A%20%20%20%20alpha3Code%0A%20%20%20%20area%0A%20%20%20%20population%0A%20%20%20%20populationDensity%0A%20%20%20%20convertedArea%28areaUnit%3A%20SQUARE_MILES%29%20%7B%0A%20%20%20%20%20%20value%0A%20%20%20%20%20%20unit%0A%20%20%20%20%20%20populationDensity%0A%20%20%20%20%7D%0A%20%20%20%20capital%0A%20%20%20%20demonym%0A%20%20%20%20gini%0A%20%20%20%20location%20%7B%0A%20%20%20%20%20%20latitude%0A%20%20%20%20%20%20longitude%0A%20%20%20%20%7D%0A%20%20%20%20numericCode%0A%20%20%20%20subregion%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20region%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20officialLanguages%20%7B%0A%20%20%20%20%20%20iso639_1%0A%20%20%20%20%20%20iso639_2%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20nativeName%0A%20%20%20%20%7D%0A%20%20%20%20currencies%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20symbol%0A%20%20%20%20%7D%0A%20%20%20%20regionalBlocs%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20acronym%0A%20%20%20%20%20%20otherAcronyms%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20otherNames%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20flag%20%7B%0A%20%20%20%20%20%20emoji%0A%20%20%20%20%20%20emojiUnicode%0A%20%20%20%20%20%20svgFile%0A%20%20%20%20%7D%0A%20%20%20%20borders%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20distanceToOtherCountries%28first%3A%205%29%20%7B%0A%20%20%20%20%20%20distanceInKm%0A%20%20%20%20%20%20countryName%0A%20%20%20%20%7D%0A%20%20%20%20shortestPathToOtherCountry%28otherCountryAlpha2Code%3A%20%22MN%22%29%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20topLevelDomains%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20callingCodes%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20alternativeSpellings%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20timezones%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%20%20nameTranslations%20%7B%0A%20%20%20%20%20%20languageCode%0A%20%20%20%20%20%20value%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A) 23 | 24 | You can also select, paginate, filter, search and order any entity, the options are endless. 25 | 26 | Once you have your desired query you can fetch the data like any other GraphQL endpoint but you probably want to use something like [appolo client](https://www.apollographql.com/docs/). 27 | 28 | ```javascript 29 | fetch('https://countries-274616.ew.r.appspot.com', { 30 | method: 'POST', 31 | headers: { 'Content-Type': 'application/json' }, 32 | body: JSON.stringify({ query: ` 33 | query { 34 | CallingCode { 35 | name 36 | countries { 37 | name 38 | } 39 | } 40 | } 41 | 42 | ` }), 43 | }) 44 | .then(res => res.json()) 45 | .then(res => console.log(res.data)); 46 | ``` 47 | 48 | ## How it works 49 | 50 | The data from [restcountries.eu](https://restcountries.eu/) is scraped with a JS script and inserted into a Neo4j graph database. Afterwards the GraphQL schema is automagically infered by the awesome [neo4j-graphql-js](https://github.com/neo4j-graphql/neo4j-graphql-js) package. We add some custom cypher queries to the schema to make the shortest path and distance data possible. Finally an Apollo server is used to create the GraphQL endpoint and playground. 51 | 52 | ## Self hosting 53 | 54 | It's quite straightforward to host the API yourself, unfortunately I can't share a read only user to the Neo4j database since I don't have a Neo4j enterprise license so you will need your own Neo4j graph database 55 | 56 | Prerequisites: 57 | 58 | * [NodeJS and npm installed](https://nodejs.org/en/download/) 59 | * [A local or cloud Neo4j graph database](https://neo4j.com/download/) 60 | * [APOC plugin installed on Neo4j](https://neo4j.com/developer/neo4j-apoc/) 61 | 62 | Getting started: 63 | 64 | 1. Clone this repo and navigate to it 65 | 2. Create an .env file on the project root folder with your Neo4j credentials and an optional apollo engine API key 66 | 67 | ``` 68 | ENGINE_API_KEY=REPLACE_ME_WITH_YOUR_APOLLO_ENGINE_API_KEY 69 | BOLT_ADDRESS=REPLACE_ME_WITH_YOUR_NEO4J_BOLT_ADDRESS 70 | DB_USERNAME=REPLACE_ME_WITH_YOUR_NEO4J_USERNAME 71 | DB_PASSWORD=REPLACE_ME_WITH_YOUR_NEO4J_PASSWORD 72 | ``` 73 | 3. Download the dependencies, run `npm install` 74 | 4. Populate the Neo4j graph database, run `npm run dataScraping`. When this command is done, you will get an infered schema file in the graphql repo, you can use this to optionally change the main schema.graphql file in the same repo 75 | 5. Start the GraphQL in dev mode, run `npm run dev` 76 | 6. Visit [http://localhost:8080/](http://localhost:8080/) to discover your self hosted API, have fun! 77 | 78 | ## Graphql voyager 79 | 80 | You can also explore the schema with [graphql-voyager](https://graph-countries-voyager.netlify.app/) 81 | -------------------------------------------------------------------------------- /blog/How to transform a rest service to a graph service.md: -------------------------------------------------------------------------------- 1 | ![Neo4j logo and GraphQL logo](./images/banner.png) 2 | 3 | # How to transform a rest service to a graph service 4 | 5 | While browsing the internet I stumbled upon an interesting [REST countries API](https://restcountries.eu/), it has a list of all the countries around the world with relevant data like regions, languages, currencies, bordering countries, timezones, location and some other cool stuff. 6 | 7 | While using it, I was quite amazed how well it worked but I wondered how it could be better. I identified the following **limitations**: 8 | 9 | 1. **Data traversal** - I can only traverse the data in one direction, country per country but what if I want to know all the subregions in Europe? Which currencies do they use in African countries? Or the full name of bordering countries? To answer those questions, I have to do multiple requests or loop over the whole country list 10 | 11 | 2. **Advanced searching** - It's possible to search by a couple of parameters but no advanced search scenario's like "give me all the countries with a population of 10 million or less" or "give me all the countries on the UTC timezone". 12 | 3. **Over fetching** - It's possible to filter the output so that you don't have to query all country-related data but you can't filter out nested data like the symbol of a currency. 13 | 14 | I don't think that the maintainers of REST countries should address these limitations. They are quite specific and natural to any traditional REST service. However, these limitations can easily be overcome by combining the power of Neo4j and GraphQL which effectively converts this REST service to a graph service. 15 | 16 | Traversing a data tree in different directions is only natural for a graph database like Neo4j. Fetching only what you need is one of the core benefits of GraphQL. Advanced searching, filtering, sorting and pagination comes for free by using the awesome open-source [neo4j-graphql.js]( https://grandstack.io/docs/neo4j-graphql-js.html ) library. 17 | 18 | I took the following steps to effectively transform REST countries to a graph service. However, the same theory could be applied to the REST service that you have in mind. You can explore the final result of the transformation in the [graph countries GitHub repo](https://github.com/lennertVanSever/graphcountries). 19 | 20 | ## Model the graph 21 | 22 | To get started you will need to insert all the data from the REST service in the Neo4j graph database. Before you do that, take a step back and think about how you will structure your graph, just drawing some circles and lines with pen and paper or a whiteboard should do the trick, once you are done with that you can take your sketch to the next level with a tool like [draw.io]([draw.io]()) 23 | 24 | ![draw io countries graph model](./images/draw-io-example.png) 25 | 26 | Deciding what should be a node, a property or a relationship can be tricky, what helps is to look at how the REST service is structured. Many-to-many or one-to-many relationships like objects in an array deserve separate nodes. Try to name all your different types of nodes and relationships as soon as possible. 27 | 28 | Most importantly, don't worry if you can't model your whole graph at this stage, graph databases are flexible and can easily be changed or extended at a later stage. 29 | 30 | ## Insert the data 31 | 32 | There are multiple ways to insert data into a Neo4j instance. If you have a big data set, on the scale of 100mb+, you probably want to use something like [Neo4j admin import](https://neo4j.com/docs/operations-manual/current/tools/import/). If the data will only be inserted via a user interface, start with modelling a GraphQL schema and follow the [Neo4j GraphQL Quickstart](https://grandstack.io/docs/neo4j-graphql-js-quickstart.html). 33 | 34 | In my use case, I already had all the necessary data and it was relatively small. With a NodeJS script, I looped over all the available countries and used the [Neo4j JavaScript driver](https://www.npmjs.com/package/neo4j-driver) to execute my Cypher queries. I could have written my GraphQL schema first but I decided to use the full flexibility of custom Cypher queries 35 | 36 | ```javascript 37 | await session.run( 38 | ` 39 | MATCH(country: Country { name: $name }) 40 | MERGE(subregion: Subregion { name: $subregion }) 41 | MERGE(region: Region { name: $region }) 42 | MERGE(country)-[:hasSubregion]->(subregion) 43 | MERGE(subregion)-[:hasRegion]->(region) 44 | `, { 45 | name, region, subregion 46 | } 47 | ) 48 | ``` 49 | 50 | The above query creates two different types of nodes and relationships. The `MERGE` clause assures that I won't have redundant nodes or relationships. Since the data set is so small, I have the luxury to drop the database and insert all the nodes and relationships again every time I execute [the script](https://github.com/lennertVanSever/graphcountries/blob/master/src/dataScraping/restCountries.js), making it really easy to make changes to my data model. 51 | 52 | ## Expose the data 53 | 54 | Now how do we expose a Neo4j database as a simple API to other applications? With the [neo4j-graphql.js]( https://grandstack.io/docs/neo4j-graphql-js.html ) library, you can easily create a GraphQL endpoint, it will handle resolvers and mutations for you and even the schema can be auto-generated by inferring GraphQL type definitions from your Neo4j database. 55 | 56 | Let's walk through the setup: 57 | 58 | Create a new directory and initiate a new node project 59 | 60 | ```bash 61 | mkdir neo4j-graphql && cd neo4j-graphql && npm init -y 62 | ``` 63 | 64 | 65 | 66 | Install all the necessary dependencies 67 | 68 | ```bash 69 | npm install dotenv apollo-server neo4j-driver neo4j-graphql-js 70 | ``` 71 | 72 | 73 | 74 | Create a `.env` file at the root of your project with the connection details of your Neo4j database 75 | 76 | ```bash 77 | BOLT_ADDRESS=bolt://localhost:7687 78 | DB_USERNAME=neo4j 79 | DB_PASSWORD=neo4j 80 | ``` 81 | 82 | 83 | 84 | Create an `index.js` file at the root of your project with the code to infer a GraphQL API from your Neo4j database 85 | 86 | ```javascript 87 | require('dotenv').config() 88 | const { ApolloServer } = require('apollo-server'); 89 | const neo4j = require('neo4j-driver'); 90 | const { inferSchema, makeAugmentedSchema } = require('neo4j-graphql-js'); 91 | 92 | const { 93 | BOLT_ADDRESS, 94 | DB_USERNAME, 95 | DB_PASSWORD 96 | } = process.env; 97 | 98 | (async () => { 99 | const driver = neo4j.driver(BOLT_ADDRESS, neo4j.auth.basic(DB_USERNAME, DB_PASSWORD)); 100 | const { typeDefs } = await inferSchema(driver); 101 | 102 | const generatedSchema = makeAugmentedSchema({ 103 | typeDefs, 104 | }); 105 | 106 | const server = new ApolloServer({ 107 | schema: generatedSchema, 108 | context: ({ req }) => { 109 | return { 110 | driver, 111 | req 112 | }; 113 | }, 114 | }); 115 | const port = process.env.PORT || 8080; 116 | 117 | server.listen(port).then(({ url }) => { 118 | console.log(`GraphQL API ready at ${url}`); 119 | }).catch(err => console.error(err)); 120 | })(); 121 | ``` 122 | 123 | 124 | 125 | Start your GraphQL API: 126 | 127 | ```bash 128 | node index.js 129 | ``` 130 | 131 | Now you can explore your GraphQL API on http://localhost:8080. Don't forget to check out the docs, the search, filter and sorting options are extensive. If you like to extend your GraphQL schema with custom Cypher queries, you can always copy the inferred schema and use the [`@cypher` directive](https://grandstack.io/docs/neo4j-graphql-js.html) 132 | 133 | ``` 134 | type Country { 135 | "The population per square kilometer" 136 | populationDensity: Float 137 | @cypher( 138 | statement: """ 139 | return this.population / this.area 140 | """ 141 | ) 142 | # ... 143 | } 144 | ``` 145 | 146 | 147 | 148 | ## Do some fun graph algorithms 149 | 150 | Try to get some insights into your data by performing some graph algorithms. I found the following queries quite interesting: 151 | 152 | The longest shortest path between all the bordering countries in the world is the following: 153 | 154 | ![longest shortest path between all bordering countries](./images/bordering-countries-all-shortest-path.png) 155 | 156 | 157 | 158 | ```cypher 159 | MATCH (a:Country), (b:Country) WHERE id(a) > id(b) 160 | MATCH p=shortestPath((a)-[:borders*]-(b)) 161 | WITH length(p) AS len, p 162 | ORDER BY len DESC LIMIT 1 163 | RETURN p 164 | ``` 165 | 166 | --- 167 | 168 | Top 10 languages that are used in different countries, notice how this correlates with the largest kingdoms in our recent history 169 | 170 | ![Top 10 official languages on a country base](./images/top-10-official-languages.png) 171 | 172 | ```cypher 173 | MATCH (country:Country) 174 | MATCH (language:Language) 175 | WITH country, language, SIZE(()-[:hasOfficialLanguage]-(language)) as countryCount 176 | ORDER BY countryCount DESC 177 | RETURN DISTINCT(language.name), countryCount 178 | LIMIT 10 179 | ``` 180 | 181 | --- 182 | 183 | You can also discover interesting patterns by just browsing your graph, for example, countries with Portuguese as there official language never border each other. 184 | 185 | ![Countries with Portuguese as there official language](./images/portuguese-official-language.png) 186 | 187 | On the oposite side, Arab speaking countries always have a bordering country that also have Arab as an official language if you exclude the island nations of Bahrain and the Comores. 188 | 189 | ![Countries with Arabic as there official language](./images/arabic-official-language.png) 190 | 191 | 192 | 193 | ## Conclusion 194 | 195 | Thanks to the generated GraphQL API, I was able to overcome the limitations of the REST API: 196 | 197 | * I can traverse the data tree in any direction, what are the subregions of Europe? [Here you go](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Region%28name%3A%20%22Europe%22%29%20%7B%0A%20%20%20%20subregions%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A). Which currencies do they use in African countries? [No problem](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Region%28name%3A%20%22Africa%22%29%20%7B%0A%20%20%20%20name%0A%20%20%20%20subregions%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20countries%28orderBy%3A%20name_asc%29%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20currencies%20%7B%0A%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20symbol%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A). What is the full name of the bordering countries? [Don't make it so easy](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%20%7B%0A%20%20%20%20name%0A%20%20%20%20borders%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A). 198 | * The search, filter and sorting options are amazing. Give me all the countries with a population of 10 million or less. [All done and sorted by population](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Country%28filter%3A%20%7B%20population_lte%3A%2010000000%20%7D%2C%20orderBy%3A%20population_desc%29%20%7B%0A%20%20%20%20name%0A%20%20%20%20population%0A%20%20%7D%0A%7D%0A). Give me all the countries on the UTC timezone. [This is the result](https://countries-274616.ew.r.appspot.com/?query=query%20%7B%0A%09Timezone%28name%3A%20%22UTC%22%29%20%7B%0A%20%20%20%20name%0A%20%20%20%20countries%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A). 199 | * Because of the nature of GraphQL, I can query exactly what I want so no under or over fetching. 200 | 201 | Next to an easy to use API, I can do some data analytics through graph algorithms to get some more insights into my domain which was impossible through the REST service. -------------------------------------------------------------------------------- /src/dataScraping/emojiFlags.json: -------------------------------------------------------------------------------- 1 | { 2 | "AD": { 3 | "emoji": "🇦🇩", 4 | "emojiUnicode": "U+1F1E6 U+1F1E9" 5 | }, 6 | "AE": { 7 | "emoji": "🇦🇪", 8 | "emojiUnicode": "U+1F1E6 U+1F1EA" 9 | }, 10 | "AF": { 11 | "emoji": "🇦🇫", 12 | "emojiUnicode": "U+1F1E6 U+1F1EB" 13 | }, 14 | "AG": { 15 | "emoji": "🇦🇬", 16 | "emojiUnicode": "U+1F1E6 U+1F1EC" 17 | }, 18 | "AI": { 19 | "emoji": "🇦🇮", 20 | "emojiUnicode": "U+1F1E6 U+1F1EE" 21 | }, 22 | "AL": { 23 | "emoji": "🇦🇱", 24 | "emojiUnicode": "U+1F1E6 U+1F1F1" 25 | }, 26 | "AM": { 27 | "emoji": "🇦🇲", 28 | "emojiUnicode": "U+1F1E6 U+1F1F2" 29 | }, 30 | "AO": { 31 | "emoji": "🇦🇴", 32 | "emojiUnicode": "U+1F1E6 U+1F1F4" 33 | }, 34 | "AQ": { 35 | "emoji": "🇦🇶", 36 | "emojiUnicode": "U+1F1E6 U+1F1F6" 37 | }, 38 | "AR": { 39 | "emoji": "🇦🇷", 40 | "emojiUnicode": "U+1F1E6 U+1F1F7" 41 | }, 42 | "AS": { 43 | "emoji": "🇦🇸", 44 | "emojiUnicode": "U+1F1E6 U+1F1F8" 45 | }, 46 | "AT": { 47 | "emoji": "🇦🇹", 48 | "emojiUnicode": "U+1F1E6 U+1F1F9" 49 | }, 50 | "AU": { 51 | "emoji": "🇦🇺", 52 | "emojiUnicode": "U+1F1E6 U+1F1FA" 53 | }, 54 | "AW": { 55 | "emoji": "🇦🇼", 56 | "emojiUnicode": "U+1F1E6 U+1F1FC" 57 | }, 58 | "AX": { 59 | "emoji": "🇦🇽", 60 | "emojiUnicode": "U+1F1E6 U+1F1FD" 61 | }, 62 | "AZ": { 63 | "emoji": "🇦🇿", 64 | "emojiUnicode": "U+1F1E6 U+1F1FF" 65 | }, 66 | "BA": { 67 | "emoji": "🇧🇦", 68 | "emojiUnicode": "U+1F1E7 U+1F1E6" 69 | }, 70 | "BB": { 71 | "emoji": "🇧🇧", 72 | "emojiUnicode": "U+1F1E7 U+1F1E7" 73 | }, 74 | "BD": { 75 | "emoji": "🇧🇩", 76 | "emojiUnicode": "U+1F1E7 U+1F1E9" 77 | }, 78 | "BE": { 79 | "emoji": "🇧🇪", 80 | "emojiUnicode": "U+1F1E7 U+1F1EA" 81 | }, 82 | "BF": { 83 | "emoji": "🇧🇫", 84 | "emojiUnicode": "U+1F1E7 U+1F1EB" 85 | }, 86 | "BG": { 87 | "emoji": "🇧🇬", 88 | "emojiUnicode": "U+1F1E7 U+1F1EC" 89 | }, 90 | "BH": { 91 | "emoji": "🇧🇭", 92 | "emojiUnicode": "U+1F1E7 U+1F1ED" 93 | }, 94 | "BI": { 95 | "emoji": "🇧🇮", 96 | "emojiUnicode": "U+1F1E7 U+1F1EE" 97 | }, 98 | "BJ": { 99 | "emoji": "🇧🇯", 100 | "emojiUnicode": "U+1F1E7 U+1F1EF" 101 | }, 102 | "BL": { 103 | "emoji": "🇧🇱", 104 | "emojiUnicode": "U+1F1E7 U+1F1F1" 105 | }, 106 | "BM": { 107 | "emoji": "🇧🇲", 108 | "emojiUnicode": "U+1F1E7 U+1F1F2" 109 | }, 110 | "BN": { 111 | "emoji": "🇧🇳", 112 | "emojiUnicode": "U+1F1E7 U+1F1F3" 113 | }, 114 | "BO": { 115 | "emoji": "🇧🇴", 116 | "emojiUnicode": "U+1F1E7 U+1F1F4" 117 | }, 118 | "BQ": { 119 | "emoji": "🇧🇶", 120 | "emojiUnicode": "U+1F1E7 U+1F1F6" 121 | }, 122 | "BR": { 123 | "emoji": "🇧🇷", 124 | "emojiUnicode": "U+1F1E7 U+1F1F7" 125 | }, 126 | "BS": { 127 | "emoji": "🇧🇸", 128 | "emojiUnicode": "U+1F1E7 U+1F1F8" 129 | }, 130 | "BT": { 131 | "emoji": "🇧🇹", 132 | "emojiUnicode": "U+1F1E7 U+1F1F9" 133 | }, 134 | "BV": { 135 | "emoji": "🇧🇻", 136 | "emojiUnicode": "U+1F1E7 U+1F1FB" 137 | }, 138 | "BW": { 139 | "emoji": "🇧🇼", 140 | "emojiUnicode": "U+1F1E7 U+1F1FC" 141 | }, 142 | "BY": { 143 | "emoji": "🇧🇾", 144 | "emojiUnicode": "U+1F1E7 U+1F1FE" 145 | }, 146 | "BZ": { 147 | "emoji": "🇧🇿", 148 | "emojiUnicode": "U+1F1E7 U+1F1FF" 149 | }, 150 | "CA": { 151 | "emoji": "🇨🇦", 152 | "emojiUnicode": "U+1F1E8 U+1F1E6" 153 | }, 154 | "CC": { 155 | "emoji": "🇨🇨", 156 | "emojiUnicode": "U+1F1E8 U+1F1E8" 157 | }, 158 | "CD": { 159 | "emoji": "🇨🇩", 160 | "emojiUnicode": "U+1F1E8 U+1F1E9" 161 | }, 162 | "CF": { 163 | "emoji": "🇨🇫", 164 | "emojiUnicode": "U+1F1E8 U+1F1EB" 165 | }, 166 | "CG": { 167 | "emoji": "🇨🇬", 168 | "emojiUnicode": "U+1F1E8 U+1F1EC" 169 | }, 170 | "CH": { 171 | "emoji": "🇨🇭", 172 | "emojiUnicode": "U+1F1E8 U+1F1ED" 173 | }, 174 | "CI": { 175 | "emoji": "🇨🇮", 176 | "emojiUnicode": "U+1F1E8 U+1F1EE" 177 | }, 178 | "CK": { 179 | "emoji": "🇨🇰", 180 | "emojiUnicode": "U+1F1E8 U+1F1F0" 181 | }, 182 | "CL": { 183 | "emoji": "🇨🇱", 184 | "emojiUnicode": "U+1F1E8 U+1F1F1" 185 | }, 186 | "CM": { 187 | "emoji": "🇨🇲", 188 | "emojiUnicode": "U+1F1E8 U+1F1F2" 189 | }, 190 | "CN": { 191 | "emoji": "🇨🇳", 192 | "emojiUnicode": "U+1F1E8 U+1F1F3" 193 | }, 194 | "CO": { 195 | "emoji": "🇨🇴", 196 | "emojiUnicode": "U+1F1E8 U+1F1F4" 197 | }, 198 | "CR": { 199 | "emoji": "🇨🇷", 200 | "emojiUnicode": "U+1F1E8 U+1F1F7" 201 | }, 202 | "CU": { 203 | "emoji": "🇨🇺", 204 | "emojiUnicode": "U+1F1E8 U+1F1FA" 205 | }, 206 | "CV": { 207 | "emoji": "🇨🇻", 208 | "emojiUnicode": "U+1F1E8 U+1F1FB" 209 | }, 210 | "CW": { 211 | "emoji": "🇨🇼", 212 | "emojiUnicode": "U+1F1E8 U+1F1FC" 213 | }, 214 | "CX": { 215 | "emoji": "🇨🇽", 216 | "emojiUnicode": "U+1F1E8 U+1F1FD" 217 | }, 218 | "CY": { 219 | "emoji": "🇨🇾", 220 | "emojiUnicode": "U+1F1E8 U+1F1FE" 221 | }, 222 | "CZ": { 223 | "emoji": "🇨🇿", 224 | "emojiUnicode": "U+1F1E8 U+1F1FF" 225 | }, 226 | "DE": { 227 | "emoji": "🇩🇪", 228 | "emojiUnicode": "U+1F1E9 U+1F1EA" 229 | }, 230 | "DJ": { 231 | "emoji": "🇩🇯", 232 | "emojiUnicode": "U+1F1E9 U+1F1EF" 233 | }, 234 | "DK": { 235 | "emoji": "🇩🇰", 236 | "emojiUnicode": "U+1F1E9 U+1F1F0" 237 | }, 238 | "DM": { 239 | "emoji": "🇩🇲", 240 | "emojiUnicode": "U+1F1E9 U+1F1F2" 241 | }, 242 | "DO": { 243 | "emoji": "🇩🇴", 244 | "emojiUnicode": "U+1F1E9 U+1F1F4" 245 | }, 246 | "DZ": { 247 | "emoji": "🇩🇿", 248 | "emojiUnicode": "U+1F1E9 U+1F1FF" 249 | }, 250 | "EC": { 251 | "emoji": "🇪🇨", 252 | "emojiUnicode": "U+1F1EA U+1F1E8" 253 | }, 254 | "EE": { 255 | "emoji": "🇪🇪", 256 | "emojiUnicode": "U+1F1EA U+1F1EA" 257 | }, 258 | "EG": { 259 | "emoji": "🇪🇬", 260 | "emojiUnicode": "U+1F1EA U+1F1EC" 261 | }, 262 | "EH": { 263 | "emoji": "🇪🇭", 264 | "emojiUnicode": "U+1F1EA U+1F1ED" 265 | }, 266 | "ER": { 267 | "emoji": "🇪🇷", 268 | "emojiUnicode": "U+1F1EA U+1F1F7" 269 | }, 270 | "ES": { 271 | "emoji": "🇪🇸", 272 | "emojiUnicode": "U+1F1EA U+1F1F8" 273 | }, 274 | "ET": { 275 | "emoji": "🇪🇹", 276 | "emojiUnicode": "U+1F1EA U+1F1F9" 277 | }, 278 | "EU": { 279 | "emoji": "🇪🇺", 280 | "emojiUnicode": "U+1F1EA U+1F1FA" 281 | }, 282 | "FI": { 283 | "emoji": "🇫🇮", 284 | "emojiUnicode": "U+1F1EB U+1F1EE" 285 | }, 286 | "FJ": { 287 | "emoji": "🇫🇯", 288 | "emojiUnicode": "U+1F1EB U+1F1EF" 289 | }, 290 | "FK": { 291 | "emoji": "🇫🇰", 292 | "emojiUnicode": "U+1F1EB U+1F1F0" 293 | }, 294 | "FM": { 295 | "emoji": "🇫🇲", 296 | "emojiUnicode": "U+1F1EB U+1F1F2" 297 | }, 298 | "FO": { 299 | "emoji": "🇫🇴", 300 | "emojiUnicode": "U+1F1EB U+1F1F4" 301 | }, 302 | "FR": { 303 | "emoji": "🇫🇷", 304 | "emojiUnicode": "U+1F1EB U+1F1F7" 305 | }, 306 | "GA": { 307 | "emoji": "🇬🇦", 308 | "emojiUnicode": "U+1F1EC U+1F1E6" 309 | }, 310 | "GB": { 311 | "emoji": "🇬🇧", 312 | "emojiUnicode": "U+1F1EC U+1F1E7" 313 | }, 314 | "GD": { 315 | "emoji": "🇬🇩", 316 | "emojiUnicode": "U+1F1EC U+1F1E9" 317 | }, 318 | "GE": { 319 | "emoji": "🇬🇪", 320 | "emojiUnicode": "U+1F1EC U+1F1EA" 321 | }, 322 | "GF": { 323 | "emoji": "🇬🇫", 324 | "emojiUnicode": "U+1F1EC U+1F1EB" 325 | }, 326 | "GG": { 327 | "emoji": "🇬🇬", 328 | "emojiUnicode": "U+1F1EC U+1F1EC" 329 | }, 330 | "GH": { 331 | "emoji": "🇬🇭", 332 | "emojiUnicode": "U+1F1EC U+1F1ED" 333 | }, 334 | "GI": { 335 | "emoji": "🇬🇮", 336 | "emojiUnicode": "U+1F1EC U+1F1EE" 337 | }, 338 | "GL": { 339 | "emoji": "🇬🇱", 340 | "emojiUnicode": "U+1F1EC U+1F1F1" 341 | }, 342 | "GM": { 343 | "emoji": "🇬🇲", 344 | "emojiUnicode": "U+1F1EC U+1F1F2" 345 | }, 346 | "GN": { 347 | "emoji": "🇬🇳", 348 | "emojiUnicode": "U+1F1EC U+1F1F3" 349 | }, 350 | "GP": { 351 | "emoji": "🇬🇵", 352 | "emojiUnicode": "U+1F1EC U+1F1F5" 353 | }, 354 | "GQ": { 355 | "emoji": "🇬🇶", 356 | "emojiUnicode": "U+1F1EC U+1F1F6" 357 | }, 358 | "GR": { 359 | "emoji": "🇬🇷", 360 | "emojiUnicode": "U+1F1EC U+1F1F7" 361 | }, 362 | "GS": { 363 | "emoji": "🇬🇸", 364 | "emojiUnicode": "U+1F1EC U+1F1F8" 365 | }, 366 | "GT": { 367 | "emoji": "🇬🇹", 368 | "emojiUnicode": "U+1F1EC U+1F1F9" 369 | }, 370 | "GU": { 371 | "emoji": "🇬🇺", 372 | "emojiUnicode": "U+1F1EC U+1F1FA" 373 | }, 374 | "GW": { 375 | "emoji": "🇬🇼", 376 | "emojiUnicode": "U+1F1EC U+1F1FC" 377 | }, 378 | "GY": { 379 | "emoji": "🇬🇾", 380 | "emojiUnicode": "U+1F1EC U+1F1FE" 381 | }, 382 | "HK": { 383 | "emoji": "🇭🇰", 384 | "emojiUnicode": "U+1F1ED U+1F1F0" 385 | }, 386 | "HM": { 387 | "emoji": "🇭🇲", 388 | "emojiUnicode": "U+1F1ED U+1F1F2" 389 | }, 390 | "HN": { 391 | "emoji": "🇭🇳", 392 | "emojiUnicode": "U+1F1ED U+1F1F3" 393 | }, 394 | "HR": { 395 | "emoji": "🇭🇷", 396 | "emojiUnicode": "U+1F1ED U+1F1F7" 397 | }, 398 | "HT": { 399 | "emoji": "🇭🇹", 400 | "emojiUnicode": "U+1F1ED U+1F1F9" 401 | }, 402 | "HU": { 403 | "emoji": "🇭🇺", 404 | "emojiUnicode": "U+1F1ED U+1F1FA" 405 | }, 406 | "ID": { 407 | "emoji": "🇮🇩", 408 | "emojiUnicode": "U+1F1EE U+1F1E9" 409 | }, 410 | "IE": { 411 | "emoji": "🇮🇪", 412 | "emojiUnicode": "U+1F1EE U+1F1EA" 413 | }, 414 | "IL": { 415 | "emoji": "🇮🇱", 416 | "emojiUnicode": "U+1F1EE U+1F1F1" 417 | }, 418 | "IM": { 419 | "emoji": "🇮🇲", 420 | "emojiUnicode": "U+1F1EE U+1F1F2" 421 | }, 422 | "IN": { 423 | "emoji": "🇮🇳", 424 | "emojiUnicode": "U+1F1EE U+1F1F3" 425 | }, 426 | "IO": { 427 | "emoji": "🇮🇴", 428 | "emojiUnicode": "U+1F1EE U+1F1F4" 429 | }, 430 | "IQ": { 431 | "emoji": "🇮🇶", 432 | "emojiUnicode": "U+1F1EE U+1F1F6" 433 | }, 434 | "IR": { 435 | "emoji": "🇮🇷", 436 | "emojiUnicode": "U+1F1EE U+1F1F7" 437 | }, 438 | "IS": { 439 | "emoji": "🇮🇸", 440 | "emojiUnicode": "U+1F1EE U+1F1F8" 441 | }, 442 | "IT": { 443 | "emoji": "🇮🇹", 444 | "emojiUnicode": "U+1F1EE U+1F1F9" 445 | }, 446 | "JE": { 447 | "emoji": "🇯🇪", 448 | "emojiUnicode": "U+1F1EF U+1F1EA" 449 | }, 450 | "JM": { 451 | "emoji": "🇯🇲", 452 | "emojiUnicode": "U+1F1EF U+1F1F2" 453 | }, 454 | "JO": { 455 | "emoji": "🇯🇴", 456 | "emojiUnicode": "U+1F1EF U+1F1F4" 457 | }, 458 | "JP": { 459 | "emoji": "🇯🇵", 460 | "emojiUnicode": "U+1F1EF U+1F1F5" 461 | }, 462 | "KE": { 463 | "emoji": "🇰🇪", 464 | "emojiUnicode": "U+1F1F0 U+1F1EA" 465 | }, 466 | "KG": { 467 | "emoji": "🇰🇬", 468 | "emojiUnicode": "U+1F1F0 U+1F1EC" 469 | }, 470 | "KH": { 471 | "emoji": "🇰🇭", 472 | "emojiUnicode": "U+1F1F0 U+1F1ED" 473 | }, 474 | "KI": { 475 | "emoji": "🇰🇮", 476 | "emojiUnicode": "U+1F1F0 U+1F1EE" 477 | }, 478 | "KM": { 479 | "emoji": "🇰🇲", 480 | "emojiUnicode": "U+1F1F0 U+1F1F2" 481 | }, 482 | "KN": { 483 | "emoji": "🇰🇳", 484 | "emojiUnicode": "U+1F1F0 U+1F1F3" 485 | }, 486 | "KP": { 487 | "emoji": "🇰🇵", 488 | "emojiUnicode": "U+1F1F0 U+1F1F5" 489 | }, 490 | "KR": { 491 | "emoji": "🇰🇷", 492 | "emojiUnicode": "U+1F1F0 U+1F1F7" 493 | }, 494 | "KW": { 495 | "emoji": "🇰🇼", 496 | "emojiUnicode": "U+1F1F0 U+1F1FC" 497 | }, 498 | "KY": { 499 | "emoji": "🇰🇾", 500 | "emojiUnicode": "U+1F1F0 U+1F1FE" 501 | }, 502 | "KZ": { 503 | "emoji": "🇰🇿", 504 | "emojiUnicode": "U+1F1F0 U+1F1FF" 505 | }, 506 | "LA": { 507 | "emoji": "🇱🇦", 508 | "emojiUnicode": "U+1F1F1 U+1F1E6" 509 | }, 510 | "LB": { 511 | "emoji": "🇱🇧", 512 | "emojiUnicode": "U+1F1F1 U+1F1E7" 513 | }, 514 | "LC": { 515 | "emoji": "🇱🇨", 516 | "emojiUnicode": "U+1F1F1 U+1F1E8" 517 | }, 518 | "LI": { 519 | "emoji": "🇱🇮", 520 | "emojiUnicode": "U+1F1F1 U+1F1EE" 521 | }, 522 | "LK": { 523 | "emoji": "🇱🇰", 524 | "emojiUnicode": "U+1F1F1 U+1F1F0" 525 | }, 526 | "LR": { 527 | "emoji": "🇱🇷", 528 | "emojiUnicode": "U+1F1F1 U+1F1F7" 529 | }, 530 | "LS": { 531 | "emoji": "🇱🇸", 532 | "emojiUnicode": "U+1F1F1 U+1F1F8" 533 | }, 534 | "LT": { 535 | "emoji": "🇱🇹", 536 | "emojiUnicode": "U+1F1F1 U+1F1F9" 537 | }, 538 | "LU": { 539 | "emoji": "🇱🇺", 540 | "emojiUnicode": "U+1F1F1 U+1F1FA" 541 | }, 542 | "LV": { 543 | "emoji": "🇱🇻", 544 | "emojiUnicode": "U+1F1F1 U+1F1FB" 545 | }, 546 | "LY": { 547 | "emoji": "🇱🇾", 548 | "emojiUnicode": "U+1F1F1 U+1F1FE" 549 | }, 550 | "MA": { 551 | "emoji": "🇲🇦", 552 | "emojiUnicode": "U+1F1F2 U+1F1E6" 553 | }, 554 | "MC": { 555 | "emoji": "🇲🇨", 556 | "emojiUnicode": "U+1F1F2 U+1F1E8" 557 | }, 558 | "MD": { 559 | "emoji": "🇲🇩", 560 | "emojiUnicode": "U+1F1F2 U+1F1E9" 561 | }, 562 | "ME": { 563 | "emoji": "🇲🇪", 564 | "emojiUnicode": "U+1F1F2 U+1F1EA" 565 | }, 566 | "MF": { 567 | "emoji": "🇲🇫", 568 | "emojiUnicode": "U+1F1F2 U+1F1EB" 569 | }, 570 | "MG": { 571 | "emoji": "🇲🇬", 572 | "emojiUnicode": "U+1F1F2 U+1F1EC" 573 | }, 574 | "MH": { 575 | "emoji": "🇲🇭", 576 | "emojiUnicode": "U+1F1F2 U+1F1ED" 577 | }, 578 | "MK": { 579 | "emoji": "🇲🇰", 580 | "emojiUnicode": "U+1F1F2 U+1F1F0" 581 | }, 582 | "ML": { 583 | "emoji": "🇲🇱", 584 | "emojiUnicode": "U+1F1F2 U+1F1F1" 585 | }, 586 | "MM": { 587 | "emoji": "🇲🇲", 588 | "emojiUnicode": "U+1F1F2 U+1F1F2" 589 | }, 590 | "MN": { 591 | "emoji": "🇲🇳", 592 | "emojiUnicode": "U+1F1F2 U+1F1F3" 593 | }, 594 | "MO": { 595 | "emoji": "🇲🇴", 596 | "emojiUnicode": "U+1F1F2 U+1F1F4" 597 | }, 598 | "MP": { 599 | "emoji": "🇲🇵", 600 | "emojiUnicode": "U+1F1F2 U+1F1F5" 601 | }, 602 | "MQ": { 603 | "emoji": "🇲🇶", 604 | "emojiUnicode": "U+1F1F2 U+1F1F6" 605 | }, 606 | "MR": { 607 | "emoji": "🇲🇷", 608 | "emojiUnicode": "U+1F1F2 U+1F1F7" 609 | }, 610 | "MS": { 611 | "emoji": "🇲🇸", 612 | "emojiUnicode": "U+1F1F2 U+1F1F8" 613 | }, 614 | "MT": { 615 | "emoji": "🇲🇹", 616 | "emojiUnicode": "U+1F1F2 U+1F1F9" 617 | }, 618 | "MU": { 619 | "emoji": "🇲🇺", 620 | "emojiUnicode": "U+1F1F2 U+1F1FA" 621 | }, 622 | "MV": { 623 | "emoji": "🇲🇻", 624 | "emojiUnicode": "U+1F1F2 U+1F1FB" 625 | }, 626 | "MW": { 627 | "emoji": "🇲🇼", 628 | "emojiUnicode": "U+1F1F2 U+1F1FC" 629 | }, 630 | "MX": { 631 | "emoji": "🇲🇽", 632 | "emojiUnicode": "U+1F1F2 U+1F1FD" 633 | }, 634 | "MY": { 635 | "emoji": "🇲🇾", 636 | "emojiUnicode": "U+1F1F2 U+1F1FE" 637 | }, 638 | "MZ": { 639 | "emoji": "🇲🇿", 640 | "emojiUnicode": "U+1F1F2 U+1F1FF" 641 | }, 642 | "NA": { 643 | "emoji": "🇳🇦", 644 | "emojiUnicode": "U+1F1F3 U+1F1E6" 645 | }, 646 | "NC": { 647 | "emoji": "🇳🇨", 648 | "emojiUnicode": "U+1F1F3 U+1F1E8" 649 | }, 650 | "NE": { 651 | "emoji": "🇳🇪", 652 | "emojiUnicode": "U+1F1F3 U+1F1EA" 653 | }, 654 | "NF": { 655 | "emoji": "🇳🇫", 656 | "emojiUnicode": "U+1F1F3 U+1F1EB" 657 | }, 658 | "NG": { 659 | "emoji": "🇳🇬", 660 | "emojiUnicode": "U+1F1F3 U+1F1EC" 661 | }, 662 | "NI": { 663 | "emoji": "🇳🇮", 664 | "emojiUnicode": "U+1F1F3 U+1F1EE" 665 | }, 666 | "NL": { 667 | "emoji": "🇳🇱", 668 | "emojiUnicode": "U+1F1F3 U+1F1F1" 669 | }, 670 | "NO": { 671 | "emoji": "🇳🇴", 672 | "emojiUnicode": "U+1F1F3 U+1F1F4" 673 | }, 674 | "NP": { 675 | "emoji": "🇳🇵", 676 | "emojiUnicode": "U+1F1F3 U+1F1F5" 677 | }, 678 | "NR": { 679 | "emoji": "🇳🇷", 680 | "emojiUnicode": "U+1F1F3 U+1F1F7" 681 | }, 682 | "NU": { 683 | "emoji": "🇳🇺", 684 | "emojiUnicode": "U+1F1F3 U+1F1FA" 685 | }, 686 | "NZ": { 687 | "emoji": "🇳🇿", 688 | "emojiUnicode": "U+1F1F3 U+1F1FF" 689 | }, 690 | "OM": { 691 | "emoji": "🇴🇲", 692 | "emojiUnicode": "U+1F1F4 U+1F1F2" 693 | }, 694 | "PA": { 695 | "emoji": "🇵🇦", 696 | "emojiUnicode": "U+1F1F5 U+1F1E6" 697 | }, 698 | "PE": { 699 | "emoji": "🇵🇪", 700 | "emojiUnicode": "U+1F1F5 U+1F1EA" 701 | }, 702 | "PF": { 703 | "emoji": "🇵🇫", 704 | "emojiUnicode": "U+1F1F5 U+1F1EB" 705 | }, 706 | "PG": { 707 | "emoji": "🇵🇬", 708 | "emojiUnicode": "U+1F1F5 U+1F1EC" 709 | }, 710 | "PH": { 711 | "emoji": "🇵🇭", 712 | "emojiUnicode": "U+1F1F5 U+1F1ED" 713 | }, 714 | "PK": { 715 | "emoji": "🇵🇰", 716 | "emojiUnicode": "U+1F1F5 U+1F1F0" 717 | }, 718 | "PL": { 719 | "emoji": "🇵🇱", 720 | "emojiUnicode": "U+1F1F5 U+1F1F1" 721 | }, 722 | "PM": { 723 | "emoji": "🇵🇲", 724 | "emojiUnicode": "U+1F1F5 U+1F1F2" 725 | }, 726 | "PN": { 727 | "emoji": "🇵🇳", 728 | "emojiUnicode": "U+1F1F5 U+1F1F3" 729 | }, 730 | "PR": { 731 | "emoji": "🇵🇷", 732 | "emojiUnicode": "U+1F1F5 U+1F1F7" 733 | }, 734 | "PS": { 735 | "emoji": "🇵🇸", 736 | "emojiUnicode": "U+1F1F5 U+1F1F8" 737 | }, 738 | "PT": { 739 | "emoji": "🇵🇹", 740 | "emojiUnicode": "U+1F1F5 U+1F1F9" 741 | }, 742 | "PW": { 743 | "emoji": "🇵🇼", 744 | "emojiUnicode": "U+1F1F5 U+1F1FC" 745 | }, 746 | "PY": { 747 | "emoji": "🇵🇾", 748 | "emojiUnicode": "U+1F1F5 U+1F1FE" 749 | }, 750 | "QA": { 751 | "emoji": "🇶🇦", 752 | "emojiUnicode": "U+1F1F6 U+1F1E6" 753 | }, 754 | "RE": { 755 | "emoji": "🇷🇪", 756 | "emojiUnicode": "U+1F1F7 U+1F1EA" 757 | }, 758 | "RO": { 759 | "emoji": "🇷🇴", 760 | "emojiUnicode": "U+1F1F7 U+1F1F4" 761 | }, 762 | "RS": { 763 | "emoji": "🇷🇸", 764 | "emojiUnicode": "U+1F1F7 U+1F1F8" 765 | }, 766 | "RU": { 767 | "emoji": "🇷🇺", 768 | "emojiUnicode": "U+1F1F7 U+1F1FA" 769 | }, 770 | "RW": { 771 | "emoji": "🇷🇼", 772 | "emojiUnicode": "U+1F1F7 U+1F1FC" 773 | }, 774 | "SA": { 775 | "emoji": "🇸🇦", 776 | "emojiUnicode": "U+1F1F8 U+1F1E6" 777 | }, 778 | "SB": { 779 | "emoji": "🇸🇧", 780 | "emojiUnicode": "U+1F1F8 U+1F1E7" 781 | }, 782 | "SC": { 783 | "emoji": "🇸🇨", 784 | "emojiUnicode": "U+1F1F8 U+1F1E8" 785 | }, 786 | "SD": { 787 | "emoji": "🇸🇩", 788 | "emojiUnicode": "U+1F1F8 U+1F1E9" 789 | }, 790 | "SE": { 791 | "emoji": "🇸🇪", 792 | "emojiUnicode": "U+1F1F8 U+1F1EA" 793 | }, 794 | "SG": { 795 | "emoji": "🇸🇬", 796 | "emojiUnicode": "U+1F1F8 U+1F1EC" 797 | }, 798 | "SH": { 799 | "emoji": "🇸🇭", 800 | "emojiUnicode": "U+1F1F8 U+1F1ED" 801 | }, 802 | "SI": { 803 | "emoji": "🇸🇮", 804 | "emojiUnicode": "U+1F1F8 U+1F1EE" 805 | }, 806 | "SJ": { 807 | "emoji": "🇸🇯", 808 | "emojiUnicode": "U+1F1F8 U+1F1EF" 809 | }, 810 | "SK": { 811 | "emoji": "🇸🇰", 812 | "emojiUnicode": "U+1F1F8 U+1F1F0" 813 | }, 814 | "SL": { 815 | "emoji": "🇸🇱", 816 | "emojiUnicode": "U+1F1F8 U+1F1F1" 817 | }, 818 | "SM": { 819 | "emoji": "🇸🇲", 820 | "emojiUnicode": "U+1F1F8 U+1F1F2" 821 | }, 822 | "SN": { 823 | "emoji": "🇸🇳", 824 | "emojiUnicode": "U+1F1F8 U+1F1F3" 825 | }, 826 | "SO": { 827 | "emoji": "🇸🇴", 828 | "emojiUnicode": "U+1F1F8 U+1F1F4" 829 | }, 830 | "SR": { 831 | "emoji": "🇸🇷", 832 | "emojiUnicode": "U+1F1F8 U+1F1F7" 833 | }, 834 | "SS": { 835 | "emoji": "🇸🇸", 836 | "emojiUnicode": "U+1F1F8 U+1F1F8" 837 | }, 838 | "ST": { 839 | "emoji": "🇸🇹", 840 | "emojiUnicode": "U+1F1F8 U+1F1F9" 841 | }, 842 | "SV": { 843 | "emoji": "🇸🇻", 844 | "emojiUnicode": "U+1F1F8 U+1F1FB" 845 | }, 846 | "SX": { 847 | "emoji": "🇸🇽", 848 | "emojiUnicode": "U+1F1F8 U+1F1FD" 849 | }, 850 | "SY": { 851 | "emoji": "🇸🇾", 852 | "emojiUnicode": "U+1F1F8 U+1F1FE" 853 | }, 854 | "SZ": { 855 | "emoji": "🇸🇿", 856 | "emojiUnicode": "U+1F1F8 U+1F1FF" 857 | }, 858 | "TC": { 859 | "emoji": "🇹🇨", 860 | "emojiUnicode": "U+1F1F9 U+1F1E8" 861 | }, 862 | "TD": { 863 | "emoji": "🇹🇩", 864 | "emojiUnicode": "U+1F1F9 U+1F1E9" 865 | }, 866 | "TF": { 867 | "emoji": "🇹🇫", 868 | "emojiUnicode": "U+1F1F9 U+1F1EB" 869 | }, 870 | "TG": { 871 | "emoji": "🇹🇬", 872 | "emojiUnicode": "U+1F1F9 U+1F1EC" 873 | }, 874 | "TH": { 875 | "emoji": "🇹🇭", 876 | "emojiUnicode": "U+1F1F9 U+1F1ED" 877 | }, 878 | "TJ": { 879 | "emoji": "🇹🇯", 880 | "emojiUnicode": "U+1F1F9 U+1F1EF" 881 | }, 882 | "TK": { 883 | "emoji": "🇹🇰", 884 | "emojiUnicode": "U+1F1F9 U+1F1F0" 885 | }, 886 | "TL": { 887 | "emoji": "🇹🇱", 888 | "emojiUnicode": "U+1F1F9 U+1F1F1" 889 | }, 890 | "TM": { 891 | "emoji": "🇹🇲", 892 | "emojiUnicode": "U+1F1F9 U+1F1F2" 893 | }, 894 | "TN": { 895 | "emoji": "🇹🇳", 896 | "emojiUnicode": "U+1F1F9 U+1F1F3" 897 | }, 898 | "TO": { 899 | "emoji": "🇹🇴", 900 | "emojiUnicode": "U+1F1F9 U+1F1F4" 901 | }, 902 | "TR": { 903 | "emoji": "🇹🇷", 904 | "emojiUnicode": "U+1F1F9 U+1F1F7" 905 | }, 906 | "TT": { 907 | "emoji": "🇹🇹", 908 | "emojiUnicode": "U+1F1F9 U+1F1F9" 909 | }, 910 | "TV": { 911 | "emoji": "🇹🇻", 912 | "emojiUnicode": "U+1F1F9 U+1F1FB" 913 | }, 914 | "TW": { 915 | "emoji": "🇹🇼", 916 | "emojiUnicode": "U+1F1F9 U+1F1FC" 917 | }, 918 | "TZ": { 919 | "emoji": "🇹🇿", 920 | "emojiUnicode": "U+1F1F9 U+1F1FF" 921 | }, 922 | "UA": { 923 | "emoji": "🇺🇦", 924 | "emojiUnicode": "U+1F1FA U+1F1E6" 925 | }, 926 | "UG": { 927 | "emoji": "🇺🇬", 928 | "emojiUnicode": "U+1F1FA U+1F1EC" 929 | }, 930 | "UM": { 931 | "emoji": "🇺🇲", 932 | "emojiUnicode": "U+1F1FA U+1F1F2" 933 | }, 934 | "US": { 935 | "emoji": "🇺🇸", 936 | "emojiUnicode": "U+1F1FA U+1F1F8" 937 | }, 938 | "UY": { 939 | "emoji": "🇺🇾", 940 | "emojiUnicode": "U+1F1FA U+1F1FE" 941 | }, 942 | "UZ": { 943 | "emoji": "🇺🇿", 944 | "emojiUnicode": "U+1F1FA U+1F1FF" 945 | }, 946 | "VA": { 947 | "emoji": "🇻🇦", 948 | "emojiUnicode": "U+1F1FB U+1F1E6" 949 | }, 950 | "VC": { 951 | "emoji": "🇻🇨", 952 | "emojiUnicode": "U+1F1FB U+1F1E8" 953 | }, 954 | "VE": { 955 | "emoji": "🇻🇪", 956 | "emojiUnicode": "U+1F1FB U+1F1EA" 957 | }, 958 | "VG": { 959 | "emoji": "🇻🇬", 960 | "emojiUnicode": "U+1F1FB U+1F1EC" 961 | }, 962 | "VI": { 963 | "emoji": "🇻🇮", 964 | "emojiUnicode": "U+1F1FB U+1F1EE" 965 | }, 966 | "VN": { 967 | "emoji": "🇻🇳", 968 | "emojiUnicode": "U+1F1FB U+1F1F3" 969 | }, 970 | "VU": { 971 | "emoji": "🇻🇺", 972 | "emojiUnicode": "U+1F1FB U+1F1FA" 973 | }, 974 | "WF": { 975 | "emoji": "🇼🇫", 976 | "emojiUnicode": "U+1F1FC U+1F1EB" 977 | }, 978 | "WS": { 979 | "emoji": "🇼🇸", 980 | "emojiUnicode": "U+1F1FC U+1F1F8" 981 | }, 982 | "XK": { 983 | "emoji": "🇽🇰", 984 | "emojiUnicode": "U+1F1FD U+1F1F0" 985 | }, 986 | "YE": { 987 | "emoji": "🇾🇪", 988 | "emojiUnicode": "U+1F1FE U+1F1EA" 989 | }, 990 | "YT": { 991 | "emoji": "🇾🇹", 992 | "emojiUnicode": "U+1F1FE U+1F1F9" 993 | }, 994 | "ZA": { 995 | "emoji": "🇿🇦", 996 | "emojiUnicode": "U+1F1FF U+1F1E6" 997 | }, 998 | "ZM": { 999 | "emoji": "🇿🇲", 1000 | "emojiUnicode": "U+1F1FF U+1F1F2" 1001 | }, 1002 | "ZW": { 1003 | "emoji": "🇿🇼", 1004 | "emojiUnicode": "U+1F1FF U+1F1FC" 1005 | } 1006 | } --------------------------------------------------------------------------------