{"id":18577479,"url":"https://github.com/ecomplus/looker-studio-connectors","last_synced_at":"2026-02-04T14:04:42.471Z","repository":{"id":233573969,"uuid":"787471945","full_name":"ecomplus/looker-studio-connectors","owner":"ecomplus","description":"Custom scripts for Looker Studio connectors to e-com.plus APIs","archived":false,"fork":false,"pushed_at":"2024-10-02T19:49:04.000Z","size":1901,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-08T16:38:30.267Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ecomplus.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-16T15:30:51.000Z","updated_at":"2024-10-03T21:09:24.000Z","dependencies_parsed_at":"2024-05-01T18:46:58.944Z","dependency_job_id":"9d5c91a4-abf1-43fb-becb-eb43069273dc","html_url":"https://github.com/ecomplus/looker-studio-connectors","commit_stats":null,"previous_names":["ecomplus/looker-studio-connectors"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ecomplus/looker-studio-connectors","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomplus%2Flooker-studio-connectors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomplus%2Flooker-studio-connectors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomplus%2Flooker-studio-connectors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomplus%2Flooker-studio-connectors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ecomplus","download_url":"https://codeload.github.com/ecomplus/looker-studio-connectors/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ecomplus%2Flooker-studio-connectors/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263283092,"owners_count":23442287,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-06T23:29:20.254Z","updated_at":"2026-02-04T14:04:42.380Z","avatar_url":"https://github.com/ecomplus.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Looker Studio connectors\n\n[:brazil: Tradução em português](./README_pt_br.md)\n\nCustom scripts for Looker Studio connectors to e-com.plus APIs\n\nInspiration: https://apptivasoftware.com/blog/bringing-json-data-from-an-api-into-looker-studio/\n\nOther references: \n\n[1] https://github.com/davidtheweiss/Apps-Script-Season-15-Data-Studio-Service\n\n[2] https://developers.google.com/looker-studio/connector/reference\n\n[3] https://developers.e-com.plus/docs/reference/\n\n[4] https://community.e-com.plus/t/aggregation-para-lista-de-produtos-mais-pedidos-em-um-determinado-periodo/3840\n\n[5] https://www.mongodb.com/docs/manual/core/aggregation-pipeline-optimization/\n\n\n# Setup\n\n**Requirements:**\n\n* [Create a project in Gclod](#a1)\n* [Configure consent screen in Gcloud Project](#a2)\n* [Create a Google appscript](#a3)\n* [Connecting appscript to looker studio](#a4)\n\n\u003cbr/\u003e\n\n\u003ca id=\"a1\"\u003e\u003c/a\u003e\n\n## Create a project in Gclod\n\nAccess the [GCloud console](https://console.cloud.google.com/)\n\n1) Click select project\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-9.png\"\u003e\n\n\u003cbr/\u003e\n\n2) Click New Project\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-10.png\"\u003e\n\n\u003cbr/\u003e\n\n3) Set a name for your project and select the organization (optional)\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-11.png\"\u003e\n\n\u003cbr/\u003e\n\n4) After creating the project, go to the search menu and search for \"oauth\" and select \"OAuth consent screen\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-12.png\"\u003e\n\n\u003cbr/\u003e\n\n\u003ca id=\"a2\"\u003e\u003c/a\u003e\n\n## Configure consent screen in Gcloud Project\n\n1) It is not necessary to select the User Type, proceed with the creation steps by filling in the mandatory data.\n\n**Steps:**\n* OAuth consent screen\n* Scopes\n* Optional info\n* Summary \n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-14.png\"\u003e\n\n\u003cbr/\u003e\n\n2) In the last step of creation click \"back to dashboard\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-18.png\"\u003e\n\n\u003cbr/\u003e\n\n3) Then click on \"Publish App\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-19.png\"\u003e\n\n\u003cbr/\u003e\n\n4) Go back to the Gcloud console home screen and copy the Project Number\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-21.png\"\u003e\n\n\u003cbr/\u003e\n\n\u003ca id=\"a3\"\u003e\u003c/a\u003e\n\n## Create a Google appscript\n\nWith the same Gcloud project it is possible to create more than one Connector\n\nIn this example, we will create a connector that will fetch all orders from your store and return the following data: **_Order number, Order ID, document number, email, customer ID, amount (total, subtotal, freight, discount), loyalty points, status and items (product ID, sku, quantity, price, final price)_** - so that they can be manipulated by Looker Studio\n\nTo create other connectors, simply make the necessary changes to the `appscript.json` and `Code.gs` files (specifically in the `schema` and the `getData()` function)\n\nLet's go...\n\nAccess the [appscript Google](https://script.google.com/)\n\n1) Click \"New Project\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-1.png\"\u003e\n\n\u003cbr/\u003e\n\n\n2) Click \"Project Settings\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-2.png\"\u003e\n\n\u003cbr/\u003e\n\n2) Enable \"Show 'appscript.json' mainfest file in editor\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-3.png\"\u003e\n\n\u003cbr/\u003e\n\n2) In \"Google Cloud Platform (GCP) Project\"\n* Click \"Change Project\"\n* Paste Project Number\n* Click \"Set Project\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-22.png\"\u003e\n\n\u003cbr/\u003e\n\n3) Go back to the \"Editor\" and select the \"appscript.json\" file\n* Paste Code \n```JSON\n{\n  \"timeZone\": \"America/Sao_Paulo\",\n  \"dependencies\": {},\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\",\n  \"dataStudio\": {\n    \"name\": \"Orders E-com Plus\",\n    \"company\": \"E-com Plus\",\n    \"logoUrl\": \"https://avatars.githubusercontent.com/u/60898319?s=200\u0026v=4\",\n    \"addonUrl\": \"https://github.com/ecomplus/looker-studio-connectors\",\n    \"supportUrl\": \"https://github.com/ecomplus/looker-studio-connectors/issues\",\n    \"description\": \"Connector for E-com Plus API to get Orders\"\n  }\n}\n```\n_**P.S.:** If it is another connector, make the necessary changes_\n\n* Save file\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-7.png\"\u003e\n\n\u003cbr/\u003e\n\n4) Now select the file \"code.gs\" and paste the code\n ```js\n // Define the schema for the data structure of the connector\nconst schema = [\n  { name: '_id', label: 'Order ID', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'number', label: 'Order Number', dataType: 'NUMBER', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'status', label: 'Status', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'buyers._id', label: 'Customer ID', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'buyers.main_email', label: 'Customer E-mail', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'buyers.doc_number', label: 'Customer CPF/CNPJ', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'amount.total', label: 'Order Total', dataType: 'NUMBER', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'amount.subtotal', label: 'Order SubTotal', dataType: 'NUMBER', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'amount.discount', label: 'Order Discount', dataType: 'NUMBER', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'amount.freight', label: 'Order Freight', dataType: 'NUMBER', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'items.product_id', label: 'Product ID', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'items.sku', label: 'Product SKU', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'items.quantity', label: 'Product Quantity', dataType: 'NUMBER', semantics: { conceptType: 'METRIC' } },\n  { name: 'items.price', label: 'Product Price', dataType: 'NUMBER', semantics: { conceptType: 'METRIC' } },\n  { name: 'items.final_price', label: 'Product Final Price', dataType: 'NUMBER', semantics: { conceptType: 'METRIC' } },\n  { name: 'loyalty_points.name', label: 'Loyalty Points Name', dataType: 'STRING', semantics: { conceptType: 'DIMENSION' } },\n  { name: 'loyalty_points.value', label: 'Loyalty Points Value', dataType: 'NUMBER', semantics: { conceptType: 'METRIC' } },\n  { name: 'created_at', label: 'Created', dataType: 'STRING', semantics: { conceptType: 'DIMENSION', semanticType: 'YEAR_MONTH_DAY_SECOND'} },\n  \n]\n\n// Return the defined schema to Data Studio\nfunction getSchema(request) {\n  return { schema: schema }\n}\n\nconst cc = DataStudioApp.createCommunityConnector()\n// Base API endpoint\nconst BASE_URL = 'https://api.e-com.plus/v1'\n\nfunction md5(inputString) {\n  return Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, inputString)\n    .reduce((output, byte) =\u003e output + (byte \u0026 255).toString(16).padStart(2, '0'), '')\n}\n\nfunction getAuthType() {\n  return cc.newAuthTypeResponse()\n    .setAuthType(cc.AuthType.USER_PASS)\n    .build()\n}\n\n// Check if the current user has administrative privileges\nfunction isAdminUser() {\n  return true\n}\n\nfunction resetAuth() {\n  var userProperties = PropertiesService.getUserProperties()\n  // userProperties.deleteProperty('dscc.key')\n  userProperties.deleteProperty('ecom.authenticationId')\n  userProperties.deleteProperty('ecom.apiKey')\n  userProperties.deleteProperty('ecom.storeId')\n  userProperties.deleteProperty('ecom.accessToken')\n  userProperties.deleteProperty('ecom.expires')\n}\n\nasync function requestAuthEcomplus(authenticationId, apiKey, storeId) {\n  const body = {\n    _id: authenticationId,\n    api_key: apiKey\n  }\n\n  const options = {\n    payload: JSON.stringify(body),\n    headers: {\n      'x-store-id': storeId\n    }\n  }\n  return UrlFetchApp.fetch(`${BASE_URL}/_authenticate.json`, options)\n}\n\nfunction getUser() {\n  const userProperties = PropertiesService.getUserProperties()\n  const authenticationId = userProperties.getProperty('ecom.authenticationId')\n  const apiKey = userProperties.getProperty('ecom.apiKey')\n  const storeId = userProperties.getProperty('ecom.storeId')\n  const accessToken = userProperties.getProperty('ecom.accessToken')\n  return {\n    userProperties,\n    authenticationId,\n    apiKey,\n    storeId,\n    accessToken\n  }\n}\n\nasync function isAuthValid() {\n  const {\n    userProperties,\n    authenticationId,\n    apiKey,\n    storeId\n  } = getUser()\n\n  let accessToken = userProperties.getProperty('ecom.accessToken')\n  const expires = userProperties.getProperty('ecom.expires')\n  if (expires) {\n    const dateExpires = new Date(expires).getTime()\n    const now = new Date().getTime()\n    if (now \u003e (dateExpires - 1 * 60 * 60 * 1000)) {\n      const resAuth = await requestAuthEcomplus(authenticationId, apiKey, storeId)\n      if (resAuth.getResponseCode() !== 200) {\n        return false\n      }\n      const responseAuth = JSON.parse(resAuth)\n      userProperties.setProperty('ecom.accessToken', responseAuth.access_token)\n      userProperties.setProperty('ecom.expires', responseAuth.expires)\n      accessToken = responseAuth.access_token\n    }\n    const headers = {\n      'x-store-id': storeId,\n      'x-my-id': authenticationId,\n      'x-access-token': accessToken\n    }\n    var res = UrlFetchApp.fetch(`${BASE_URL}/authentications/me.json`, { headers,  'muteHttpExceptions': true })\n    return res.getResponseCode() === 200\n  } else {\n    return false\n  }\n}\n\nasync function setCredentials(request) {\n  const rawUserParts = request.userPass.username.split(':')\n  let storeId = 0\n  let authUsername = ''\n  let authId = ''\n  if (rawUserParts.length === 2) {\n    storeId = Number(rawUserParts[0])\n    authId = rawUserParts[1]\n  } else {\n    authUsername = rawUserParts[0]\n  }\n  const rawPass = request.userPass.password\n  let authApiKey = storeId \u0026\u0026 rawPass.length === 128 ? rawPass : ''\n  const authPassMd5 = authApiKey ? '' : md5(rawPass);\n  let loginData\n  if (!authApiKey) {\n    const resp = UrlFetchApp.fetch(`${BASE_URL}/_login.json`, {\n      payload: JSON.stringify({\n        username: authUsername,\n        pass_md5_hash: authPassMd5,\n      }),\n      method: 'POST',\n      headers: {\n        'content-type': 'application/json',\n        'x-store-id': storeId\n      },\n    });\n    if (resp.getResponseCode() !== 200) {\n      return cc.newSetCredentialsResponse()\n        .setIsValid(false)\n        .build()\n    }\n    loginData = JSON.parse(resp)\n  }\n  if (loginData) {\n    storeId = loginData.store_id\n    authId = loginData._id\n    authApiKey = loginData.api_key\n  }\n\n  var userProperties = PropertiesService.getUserProperties()\n  userProperties.setProperty('ecom.authenticationId', authId)\n  userProperties.setProperty('ecom.apiKey', authApiKey)\n  userProperties.setProperty('ecom.storeId', `${storeId}`)\n  \n  const resAuth = await requestAuthEcomplus(authId, authApiKey, `${storeId}`)\n  if (resAuth.getResponseCode() !== 200) {\n    return cc.newSetCredentialsResponse()\n      .setIsValid(false)\n      .build()\n  }\n  const responseAuth = JSON.parse(resAuth)\n  userProperties.setProperty('ecom.accessToken', responseAuth.access_token)\n  userProperties.setProperty('ecom.expires', responseAuth.expires)\n\n  return cc.newSetCredentialsResponse()\n    .setIsValid(true)\n    .build()\n}\n\nfunction getConfig(request) {\n\n}\n\nfunction getData(request) {\n  if (isAuthValid()) {\n    const {\n      userProperties,\n      authenticationId,\n      storeId,\n      accessToken\n    } = getUser()\n\n    const dataSchema = schema.reduce((init, current) =\u003e {\n      const isExist = request.fields.find(field =\u003e field.name === current.name)\n      if (isExist) {\n        init.push(current)\n      }\n      return init\n    }, [])\n\n    const listOrdersUrl = BASE_URL + '/$aggregate.json'\n    const headers = {\n      'X-Access-Token': accessToken,\n      'X-Store-ID': storeId,\n      'X-My-ID': authenticationId\n    }\n\n    const body= {\n     \"resource\": \"orders\",\n      \"pipeline\": [\n         {\n            \"$project\": {\n                \"number\": 1,\n                \"status\": 1,\n                \"buyers.doc_number\": 1,\n                \"buyers.main_email\": 1,\n                \"buyers._id\": 1,\n                \"amount\": 1,\n                \"loyalty_points\": 1,\n                \"items.product_id\": 1,\n                \"items.sku\": 1,\n                \"items.quantity\": 1,\n                \"items.price\": 1,\n                \"items.final_price\": 1,\n                \"created_at\": 1\n            }\n        },\n        {\n            \"$unwind\": {\n                \"path\": \"$items\",\n                \"preserveNullAndEmptyArrays\": true\n            }\n        }\n      ]\n    }\n\n    const options = {\n      headers,\n      payload: JSON.stringify(body),\n      method: 'POST'\n    }\n\n    const resAggregation = UrlFetchApp.fetch(listOrdersUrl, options)\n    const parseAggregation = JSON.parse(resAggregation)\n    const aggregation = parseAggregation.result\n   \n    const rows = aggregation.map(order =\u003e {\n      const values = dataSchema.map(field =\u003e {\n        if(field.name.startsWith('buyers')){\n          const fields = field.name.split('.')\n          const customer = order[fields[0]] \u0026\u0026 order[fields[0]].length \u0026\u0026 order[fields[0]][0][fields[1]]\n          return customer ? customer : (field.dataType === 'NUMBER' ? 0 : '') \n        }\n        if(field.name.startsWith('loyalty_points')){\n          const fields = field.name.split('.')\n          const loyaltyPoints = order[fields[0]]\n          const keys = Object.keys(loyaltyPoints)\n          category[keys[0]][fields[1]]\n\n          return loyaltyPoints \u0026\u0026 keys \u0026\u0026 keys.length\n            ? loyaltyPoints[keys[0]][fields[1]]\n            : field.dataType === 'NUMBER' ? 0 : ''\n        }\n        if(field.name.startsWith('amount')){\n          const fields = field.name.split('.')\n          const amount = order[fields[0]] \u0026\u0026 order[fields[0]][fields[1]]\n          return amount || 0\n        }\n        if(field.name.startsWith('items')){\n          const fields = field.name.split('.')\n          const amount = order[fields[0]] \u0026\u0026 order[fields[0]][fields[1]]\n          return amount || 0\n        }\n        if(field.name.startsWith('created_at')){\n          const dateString = order[field.name].split('T')\n          let date = dateString[0].replaceAll('-','')\n          date += dateString[1].split('.')[0].replaceAll(':','')\n          return date\n        }\n        return order[field.name] || typeof order[field.name] === 'number' ? order[field.name] : ''\n      })\n      return { values }\n    })\n\n    return {\n      schema: dataSchema,\n      rows\n    }\n  }\n  return {\n    schema: [],\n    rows: []\n  }\n}\n\n ```\n _**P.S.:** If it is another connector, make the necessary changes_\n\n * Save file\n\n \u003cbr/\u003e \n\n 5) To deploy the connector click on deploy\n\n* Click \"New Deployment\"\n* Click \"Select Type\"\n* And select _Add-on_ and _Library_\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-25.png\"\u003e\n\n\u003cbr/\u003e\n\n* Add a description\n* And click \"Deploy\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-26.png\"\u003e\n\n\u003cbr/\u003e\n\n6) Copy \"Deploymnet ID\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-27.png\"\u003e\n\n\u003cbr/\u003e\n\n\u003ca id=\"a4\"\u003e\u003c/a\u003e\n\n## Connecting AppScript to Looker Studio\n\nAccess the [Looker Studio](https://lookerstudio.google.com/)\n\n1) Click \"Blank Report\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-28.png\"\u003e\n\n\u003cbr/\u003e\n\n2) Click \"Add Data\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-29.png\"\u003e\n\n\u003cbr/\u003e\n\n3) Scroll the page to \"Partner Connector\" and wait for it to load, select \"Build Your Own\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-30.png\"\u003e\n\n\u003cbr/\u003e\n\n4) Paste your \"Deployment ID\" and click \"Validate\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-31.png\"\u003e\n\n\u003cbr/\u003e\n\n5) Select the connector\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-32.png\"\u003e\n\n\u003cbr/\u003e\n\n6) Click \"Authorize\", then you will be redirected to the consent screen you initially created.\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-33.png\"\u003e\n\n\u003cbr/\u003e\n\n7) As your app is private and not yet fully approved, this security screen will appear, just authorize your own app by clicking on \"Advenced\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-34.png\"\u003e\n\n\u003cbr/\u003e\n\n8) And then \"Go to your_app_name\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-35.png\"\u003e\n\n\u003cbr/\u003e\n\n9) Finalize consent by clicking \"Allow\"\n\n10) If everything goes well you will see a login screen for the created connector, here you will enter access credentials to your store at E-com Plus, and click \"Submit\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-37.png\"\u003e\n\n\u003cbr/\u003e\n\n11) If your access is successful, click on \"Add\",\n\nYour connector is installed in your \"Report\"\n\n\u003cbr/\u003e\n\u003cimg src=\"assets/images/app-script-40.png\"\u003e\n\n\u003cbr/\u003e\n\n## More\n\nTo create other connectors, repeat steps:\n* [Create a Google appscript](#a3)\n* [Connecting appscript to looker studio](#a4)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecomplus%2Flooker-studio-connectors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fecomplus%2Flooker-studio-connectors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fecomplus%2Flooker-studio-connectors/lists"}