├── server ├── .eslintignore ├── .gitignore ├── dist │ ├── index.d.ts │ ├── services │ │ ├── MetricService │ │ │ ├── types │ │ │ │ ├── index.js │ │ │ │ ├── index.js.map │ │ │ │ └── index.d.ts │ │ │ ├── MetricService.js │ │ │ ├── MetricService.js.map │ │ │ ├── index.d.ts │ │ │ ├── utils │ │ │ │ ├── getProfileVariablesFromEnvironment.d.ts │ │ │ │ ├── getProfileVariablesFromEnvironment.js.map │ │ │ │ └── getProfileVariablesFromEnvironment.js │ │ │ ├── index.js.map │ │ │ ├── index.js │ │ │ ├── MetricService.d.ts │ │ │ ├── DbtLocalMetricService.d.ts │ │ │ ├── DbtLocalMetricService.js.map │ │ │ └── DbtLocalMetricService.js │ │ ├── gitService.d.ts │ │ ├── dbtService.d.ts │ │ ├── gitService.js.map │ │ ├── metricService.d.ts │ │ ├── dbtService.js.map │ │ ├── dbtService.js │ │ ├── metricService.js.map │ │ ├── gitService.js │ │ └── metricService.js │ ├── routes │ │ ├── metrics.d.ts │ │ ├── graphql.d.ts │ │ ├── metrics.js.map │ │ ├── metrics.js │ │ ├── graphql.js.map │ │ └── graphql.js │ ├── index.js.map │ └── index.js ├── .eslintrc.json ├── .prettierrc.js ├── run_docker.sh ├── start.sh ├── tsconfig.json ├── src │ ├── services │ │ ├── MetricService │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ └── getProfileVariablesFromEnvironment.ts │ │ │ └── DbtLocalMetricService.ts │ │ └── gitService.ts │ ├── index.ts │ └── routes │ │ ├── metrics.ts │ │ └── graphql.ts ├── .example.env └── package.json ├── .dockerignore ├── .gitignore ├── heroku.yml ├── packages.yml ├── app.json ├── Dockerfile ├── macros └── run_metric.sql ├── dbt_project.yml ├── README.md └── LICENSE /server/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | server/node_modules 2 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | dbt_packages/ 3 | logs/ 4 | -------------------------------------------------------------------------------- /server/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: Dockerfile 4 | -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/" 3 | } 4 | -------------------------------------------------------------------------------- /packages.yml: -------------------------------------------------------------------------------- 1 | packages: 2 | - package: dbt-labs/metrics 3 | version: [">=0.1.2"] -------------------------------------------------------------------------------- /server/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json') 3 | } 4 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/types/index.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /server/run_docker.sh: -------------------------------------------------------------------------------- 1 | docker build -t metrics_api . 2 | docker run --env-file .env.local metrics_api -------------------------------------------------------------------------------- /server/dist/services/MetricService/MetricService.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=MetricService.js.map -------------------------------------------------------------------------------- /server/dist/routes/metrics.d.ts: -------------------------------------------------------------------------------- 1 | declare const router: import("express-serve-static-core").Router; 2 | export default router; 3 | -------------------------------------------------------------------------------- /server/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ssh-keygen -b 4096 -f ~/.ssh/id_rsa -q -N '' 4 | cat ~/.ssh/id_rsa.pub 5 | node dist/index.js -------------------------------------------------------------------------------- /server/dist/services/MetricService/types/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/services/MetricService/types/index.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /server/dist/services/MetricService/MetricService.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"MetricService.js","sourceRoot":"","sources":["../../../src/services/MetricService/MetricService.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /server/dist/routes/graphql.d.ts: -------------------------------------------------------------------------------- 1 | declare const router: import("express-serve-static-core").Router; 2 | export declare function graphqlInit(): { 3 | success: boolean; 4 | }; 5 | export default router; 6 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/index.d.ts: -------------------------------------------------------------------------------- 1 | import DbtLocalMetricService from './DbtLocalMetricService.js'; 2 | declare const metricService: DbtLocalMetricService; 3 | export * from './types/index.js'; 4 | export default metricService; 5 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/utils/getProfileVariablesFromEnvironment.d.ts: -------------------------------------------------------------------------------- 1 | import { DbtProfile } from '../DbtLocalMetricService.js'; 2 | declare const getProfileVariablesFromEnv: () => DbtProfile | undefined; 3 | export default getProfileVariablesFromEnv; 4 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbt Metrics API", 3 | "description": "A REST/GraphQL API for your dbt metrics layer", 4 | "repository": "https://github.com/mjirv/dbt-metrics-api", 5 | "logo": "https://node-js-sample.herokuapp.com/node.png", 6 | "keywords": ["dbt", "metrics", "api", "express"], 7 | "stack": "container" 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM node:18 3 | 4 | WORKDIR /usr/app 5 | COPY ./server/ /usr/app 6 | 7 | RUN apt-get update && apt-get install -y \ 8 | python3 \ 9 | python3-pip 10 | RUN python3 -m pip install dbt-core dbt-snowflake dbt-redshift dbt-bigquery dbt-postgres 11 | 12 | RUN NODE_ENV=production npm install 13 | 14 | CMD ["node", "dist/index.js"] 15 | EXPOSE $PORT -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "lib": ["es2020"], 9 | "module": "esnext", 10 | "target": "es2017" 11 | }, 12 | "include": [ 13 | "src/**/*.ts", 14 | "test/**/*.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /server/dist/services/gitService.d.ts: -------------------------------------------------------------------------------- 1 | interface IGitService { 2 | dir: string | undefined; 3 | clone: (repository: string) => void; 4 | } 5 | export declare class GithubService implements IGitService { 6 | #private; 7 | dir: string | undefined; 8 | constructor(accessToken?: string); 9 | clone(repository: string): Promise; 10 | } 11 | declare const instance: GithubService; 12 | export default instance; 13 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/services/MetricService/index.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,kBAAkB,CAAC;AAC1C,OAAO,qBAAqB,MAAM,4BAA4B,CAAC;AAC/D,OAAO,0BAA0B,MAAM,+CAA+C,CAAC;AAEvF,MAAM,gBAAgB,GAAG,0BAA0B,EAAE,CAAC;AACtD,MAAM,aAAa,GAAG,IAAI,qBAAqB,CAAC;IAC9C,cAAc,EAAE,UAAU,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,UAAU;IAC5E,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU;IAC9B,gBAAgB;CACjB,CAAC,CAAC;AAEH,cAAc,kBAAkB,CAAC;AACjC,eAAe,aAAa,CAAC"} -------------------------------------------------------------------------------- /macros/run_metric.sql: -------------------------------------------------------------------------------- 1 | {% macro run_metric(metric_name, grain, dimensions=[], start_date=None, end_date=None, secondary_calculations=[], format='json') %} 2 | {% set res = run_query("select * from " ~ metrics.calculate(metric(metric_name), grain=grain, dimensions=dimensions, secondary_calculations=secondary_calculations, start_date=start_date, end_date=end_date)) %} 3 | 4 | {% do log("<<>>", info=True) %} 5 | {% if format == 'csv' %} 6 | {% do res.print_csv() %} 7 | {% else %} 8 | {% do res.print_json() %} 9 | {% endif %} 10 | {% endmacro %} 11 | -------------------------------------------------------------------------------- /server/src/services/MetricService/index.ts: -------------------------------------------------------------------------------- 1 | import gitService from '../gitService.js'; 2 | import DbtLocalMetricService from './DbtLocalMetricService.js'; 3 | import getProfileVariablesFromEnv from './utils/getProfileVariablesFromEnvironment.js'; 4 | 5 | const profileVariables = getProfileVariablesFromEnv(); 6 | const metricService = new DbtLocalMetricService({ 7 | dbtProjectPath: gitService.dir || process.env.DBT_PROJECT_PATH || '../../..', 8 | target: process.env.DBT_TARGET, 9 | profileVariables, 10 | }); 11 | 12 | export * from './types/index.js'; 13 | export default metricService; 14 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/index.js: -------------------------------------------------------------------------------- 1 | import gitService from '../gitService.js'; 2 | import DbtLocalMetricService from './DbtLocalMetricService.js'; 3 | import getProfileVariablesFromEnv from './utils/getProfileVariablesFromEnvironment.js'; 4 | const profileVariables = getProfileVariablesFromEnv(); 5 | const metricService = new DbtLocalMetricService({ 6 | dbtProjectPath: gitService.dir || process.env.DBT_PROJECT_PATH || '../../..', 7 | target: process.env.DBT_TARGET, 8 | profileVariables, 9 | }); 10 | export * from './types/index.js'; 11 | export default metricService; 12 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /server/dist/services/dbtService.d.ts: -------------------------------------------------------------------------------- 1 | export interface DBTResource { 2 | name: string; 3 | label: string; 4 | description: string; 5 | type: string; 6 | time_grains: string; 7 | dimensions: string[]; 8 | filters: string[]; 9 | unique_id: string; 10 | model: string; 11 | package_name: string; 12 | } 13 | interface Selectors { 14 | type?: string; 15 | model?: string; 16 | package_name?: string; 17 | } 18 | export declare const listMetrics: (name?: string | undefined, selectors?: Selectors) => DBTResource[]; 19 | export declare type Grain = 'day' | 'week' | 'month' | 'quarter' | 'year'; 20 | interface QueryParams { 21 | metric_name: string; 22 | grain: Grain; 23 | dimensions?: string[]; 24 | start_date?: string; 25 | end_date?: string; 26 | format?: 'csv' | 'json'; 27 | } 28 | export declare const queryMetric: (params: QueryParams) => string; 29 | export {}; 30 | -------------------------------------------------------------------------------- /server/src/services/MetricService/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Grain = 'day' | 'week' | 'month' | 'quarter' | 'year'; 2 | 3 | export interface QueryParams { 4 | metric_name: string; 5 | grain: Grain; 6 | dimensions?: string[]; 7 | start_date?: string; 8 | end_date?: string; 9 | format?: 'csv' | 'json'; 10 | } 11 | 12 | export interface DBTResource { 13 | name: string; 14 | label: string; 15 | description: string; 16 | type: string; 17 | time_grains: Grain[]; 18 | dimensions: string[]; 19 | filters: string[]; 20 | unique_id: string; 21 | model: string; 22 | package_name: string; 23 | } 24 | 25 | export interface Selectors { 26 | type?: string; 27 | model?: string; 28 | package_name?: string; 29 | } 30 | 31 | export interface MetricService { 32 | listMetrics: (name?: string, selectors?: Selectors) => DBTResource[]; 33 | queryMetric: (params: QueryParams) => Record; 34 | } 35 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/MetricService.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Grain = 'day' | 'week' | 'month' | 'quarter' | 'year'; 2 | export interface QueryParams { 3 | metric_name: string; 4 | grain: Grain; 5 | dimensions?: string[]; 6 | start_date?: string; 7 | end_date?: string; 8 | format?: 'csv' | 'json'; 9 | } 10 | export interface DBTResource { 11 | name: string; 12 | label: string; 13 | description: string; 14 | type: string; 15 | time_grains: string; 16 | dimensions: string[]; 17 | filters: string[]; 18 | unique_id: string; 19 | model: string; 20 | package_name: string; 21 | } 22 | export interface Selectors { 23 | type?: string; 24 | model?: string; 25 | package_name?: string; 26 | } 27 | export interface MetricService { 28 | listMetrics: (name?: string, selectors?: Selectors) => DBTResource[]; 29 | queryMetric: (params: QueryParams) => Record; 30 | } 31 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Grain = 'day' | 'week' | 'month' | 'quarter' | 'year'; 2 | export interface QueryParams { 3 | metric_name: string; 4 | grain: Grain; 5 | dimensions?: string[]; 6 | start_date?: string; 7 | end_date?: string; 8 | format?: 'csv' | 'json'; 9 | } 10 | export interface DBTResource { 11 | name: string; 12 | label: string; 13 | description: string; 14 | type: string; 15 | time_grains: Grain[]; 16 | dimensions: string[]; 17 | filters: string[]; 18 | unique_id: string; 19 | model: string; 20 | package_name: string; 21 | } 22 | export interface Selectors { 23 | type?: string; 24 | model?: string; 25 | package_name?: string; 26 | } 27 | export interface MetricService { 28 | listMetrics: (name?: string, selectors?: Selectors) => DBTResource[]; 29 | queryMetric: (params: QueryParams) => Record; 30 | } 31 | -------------------------------------------------------------------------------- /server/dist/services/gitService.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"gitService.js","sourceRoot":"","sources":["../../src/services/gitService.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,SAAsB,MAAM,YAAY,CAAC;AAChD,qDAAqD;AACrD,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,MAAM,OAAO,aAAa;IAKxB,YAAY,WAAoB;QAJhC,6CAAsB;QACtB,wCAAmB;QAIjB,IAAI,CAAC,WAAW,EAAE;YAChB,OAAO,CAAC,KAAK,CACX,qEAAqE,CACtE,CAAC;SACH;QACD,uBAAA,IAAI,8BAAgB,WAAW,MAAA,CAAC;QAChC,uBAAA,IAAI,yBAAW,SAAS,EAAE,MAAA,CAAC;QAC3B,IAAI,CAAC,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,EAAC,MAAM,EAAE,SAAS,EAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,UAAkB;QAC5B,IAAI,CAAC,uBAAA,IAAI,kCAAa,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YACnC,MAAM,KAAK,CACT,+DAA+D,CAChE,CAAC;SACH;QACD,MAAM,GAAG,GAAG,WAAW,uBAAA,IAAI,kCAAa,eAAe,UAAU,MAAM,CAAC;QACxE,MAAM,uBAAA,IAAI,6BAAQ,CAAC,GAAG,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1E,CAAC;CACF;;AAED,MAAM,QAAQ,GAAG,IAAI,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;AACpE,eAAe,QAAQ,CAAC"} -------------------------------------------------------------------------------- /server/.example.env: -------------------------------------------------------------------------------- 1 | ### This is an example environment file ### 2 | ### Copy to .env and uncomment the parts you need ### 3 | 4 | # The port the API will run on 5 | PORT=3001 6 | 7 | ### LOCAL ENVIRONMENT VARIABLES ### 8 | 9 | # Uncomment this to use a different dbt target than your default 10 | # DBT_TARGET= 'dev' 11 | 12 | # The path to your dbt project 13 | # DBT_PROJECT_PATH=../../.. 14 | 15 | ### PRODUCTION ENVIRONMENT VARIABLES ### 16 | 17 | GITHUB_REPOSITORY=mjirv/dbt-demo-project 18 | GITHUB_ACCESS_TOKEN="shh_its_a_secret" 19 | 20 | # Note: see dbt's warehouse profiles documentation for all available fields 21 | # https://docs.getdbt.com/reference/warehouse-profiles/postgres-profile 22 | DBT_PROFILE_TYPE=postgres 23 | DBT_PROFILE_USER=dbt 24 | DBT_PROFILE_PASSWORD=some_secret_password 25 | DBT_PROFILE_HOST=localhost 26 | DBT_PROFILE_PORT=5432 27 | DBT_PROFILE_DBNAME=michael 28 | DBT_PROFILE_SCHEMA=jaffle_shop 29 | 30 | # Authentication: Only uncomment these if using Kable (https://kable.io) for authentication, and replace it with your own ID/secret 31 | # KABLE_CLIENT_ID=some_kable_client_id 32 | # KABLE_CLIENT_SECRET=some_kable_client_secret 33 | # KABLE_ENV=TEST -------------------------------------------------------------------------------- /server/dist/services/metricService.d.ts: -------------------------------------------------------------------------------- 1 | export interface MetricService { 2 | listMetrics: (name?: string, selectors?: Selectors) => DBTResource[]; 3 | queryMetric: (params: QueryParams) => Record; 4 | } 5 | export interface DBTResource { 6 | name: string; 7 | label: string; 8 | description: string; 9 | type: string; 10 | time_grains: string; 11 | dimensions: string[]; 12 | filters: string[]; 13 | unique_id: string; 14 | model: string; 15 | package_name: string; 16 | } 17 | interface Selectors { 18 | type?: string; 19 | model?: string; 20 | package_name?: string; 21 | } 22 | export declare const installMetricsPackage: () => void; 23 | export declare const listMetrics: (name?: string | undefined, selectors?: Selectors) => DBTResource[]; 24 | export declare type Grain = 'day' | 'week' | 'month' | 'quarter' | 'year'; 25 | interface QueryParams { 26 | metric_name: string; 27 | grain: Grain; 28 | dimensions?: string[]; 29 | start_date?: string; 30 | end_date?: string; 31 | format?: 'csv' | 'json'; 32 | } 33 | export declare const queryMetric: (params: QueryParams) => string; 34 | export {}; 35 | -------------------------------------------------------------------------------- /server/src/services/gitService.ts: -------------------------------------------------------------------------------- 1 | import simpleGit, {SimpleGit} from 'simple-git'; 2 | // eslint-disable-next-line node/no-extraneous-import 3 | import tempy from 'tempy'; 4 | 5 | interface IGitService { 6 | dir: string | undefined; 7 | clone: (repository: string) => void; 8 | } 9 | 10 | export class GithubService implements IGitService { 11 | #accessToken?: string; 12 | #client: SimpleGit; 13 | dir: string | undefined; 14 | 15 | constructor(accessToken?: string) { 16 | if (!accessToken) { 17 | console.debug( 18 | 'no access token provided. initializing blank GithubService instance' 19 | ); 20 | } 21 | this.#accessToken = accessToken; 22 | this.#client = simpleGit(); 23 | this.dir = accessToken ? tempy.directory({prefix: '_github'}) : undefined; 24 | } 25 | 26 | async clone(repository: string) { 27 | if (!this.#accessToken || !this.dir) { 28 | throw Error( 29 | 'Cannot clone, no access token was provided in the environment' 30 | ); 31 | } 32 | const url = `https://${this.#accessToken}@github.com/${repository}.git`; 33 | await this.#client.env('GIT_TERMINAL_PROMPT', '0').clone(url, this.dir); 34 | } 35 | } 36 | 37 | const instance = new GithubService(process.env.GITHUB_ACCESS_TOKEN); 38 | export default instance; 39 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbt-metrics-api", 3 | "version": "0.2.1", 4 | "description": "", 5 | "exports": "./dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "engines": { 8 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 9 | }, 10 | "files": [ 11 | "dist" 12 | ], 13 | "license": "Apache-2.0", 14 | "keywords": [], 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1", 17 | "lint": "gts lint", 18 | "clean": "gts clean", 19 | "compile": "tsc", 20 | "fix": "gts fix", 21 | "prepublishOnly": "npm run compile", 22 | "pretest": "npm run compile", 23 | "posttest": "npm run lint" 24 | }, 25 | "devDependencies": { 26 | "@types/body-parser": "^1.19.2", 27 | "@types/cors": "^2.8.12", 28 | "@types/express": "^4.17.13", 29 | "@types/js-yaml": "^4.0.5", 30 | "@types/morgan": "^1.9.3", 31 | "@types/node": "^14.11.2", 32 | "gts": "^3.1.0", 33 | "typescript": "^4.0.3" 34 | }, 35 | "dependencies": { 36 | "body-parser": "^1.19.2", 37 | "cors": "^2.8.5", 38 | "dotenv": "^16.0.0", 39 | "express": "^4.17.3", 40 | "express-graphql": "^0.12.0", 41 | "graphql": "^15.8.0", 42 | "helmet": "^5.0.2", 43 | "js-yaml": "^4.1.0", 44 | "kable-node-express": "^2.3.13", 45 | "morgan": "^1.10.0", 46 | "simple-git": "^3.6.0", 47 | "tempy": "^2.0.0" 48 | }, 49 | "type": "module" 50 | } 51 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/utils/getProfileVariablesFromEnvironment.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"getProfileVariablesFromEnvironment.js","sourceRoot":"","sources":["../../../../src/services/MetricService/utils/getProfileVariablesFromEnvironment.ts"],"names":[],"mappings":"AAEA,MAAM,0BAA0B,GAAG,GAA2B,EAAE;IAC9D,MAAM,eAAe,GAAG;QACtB,MAAM;QACN,UAAU;QACV,MAAM;QACN,YAAY;QACZ,gBAAgB;QAChB,aAAa;QACb,cAAc;QACd,WAAW;QACX,UAAU;QACV,WAAW;QACX,6BAA6B;QAC7B,sBAAsB;KACvB,CAAC;IAEF,MAAM,gBAAgB,GAAG,MAAM,CAAC,WAAW,CACzC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;SACxB,MAAM,CACL,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CACR,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC;QAC9B,CAAC,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAC7D;SACA,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;QACrB,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE;QAC7C,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK;KACvB,CAAC,CACL,CAAC;IAEF,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CACpC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;SACxB,MAAM,CACL,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CACR,GAAG,CAAC,UAAU,CAAC,cAAc,CAAC;QAC9B,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,CAC5D;SACA,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;QACrB,GAAG,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE;QAC7C,KAAK;KACN,CAAC,CACqB,CAAC;IAE5B,OAAO,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,MAAM,GAAG,CAAC;QAC7C,CAAC,iCACM,gBAAgB,KACnB,IAAI,EAAE,gBAAgB,CAAC,IAAiB,EACxC,WAAW,oBAAM,WAAW,KAEhC,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC"} -------------------------------------------------------------------------------- /dbt_project.yml: -------------------------------------------------------------------------------- 1 | # Name your project! Project names should contain only lowercase characters 2 | # and underscores. A good package name should reflect your organization's 3 | # name or the intended use of these models 4 | name: "dbt_metrics_api" 5 | version: "0.2.1" 6 | config-version: 2 7 | 8 | # This setting configures which "profile" dbt uses for this project. 9 | profile: "dbt-metrics-api" 10 | 11 | # These configurations specify where dbt should look for different types of files. 12 | # The `model-paths` config, for example, states that models in this project can be 13 | # found in the "models/" directory. You probably won't need to change these! 14 | model-paths: ["models"] 15 | analysis-paths: ["analyses"] 16 | test-paths: ["tests"] 17 | seed-paths: ["seeds"] 18 | macro-paths: ["macros"] 19 | snapshot-paths: ["snapshots"] 20 | 21 | target-path: "target" # directory which will store compiled SQL files 22 | clean-targets: # directories to be removed by `dbt clean` 23 | - "target" 24 | - "dbt_packages" 25 | 26 | # Configuring models 27 | # Full documentation: https://docs.getdbt.com/docs/configuring-models 28 | 29 | # In this example config, we tell dbt to build all models in the example/ directory 30 | # as tables. These settings can be overridden in the individual model files 31 | # using the `{{ config(...) }}` macro. 32 | models: 33 | dbt-metrics-api: 34 | # Config indicated by + and applies to all files under models/example/ 35 | example: 36 | +materialized: view 37 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/utils/getProfileVariablesFromEnvironment.js: -------------------------------------------------------------------------------- 1 | const getProfileVariablesFromEnv = () => { 2 | const CREDENTIAL_KEYS = [ 3 | 'USER', 4 | 'PASSWORD', 5 | 'TYPE', 6 | 'PROJECT_ID', 7 | 'PRIVATE_KEY_ID', 8 | 'PRIVATE_KEY', 9 | 'CLIENT_EMAIL', 10 | 'CLIENT_ID', 11 | 'AUTH_URI', 12 | 'TOKEN_URI', 13 | 'AUTH_PROVIDER_X509_CERT_URL', 14 | 'CLIENT_X509_CERT_URL', 15 | ]; 16 | const profileVariables = Object.fromEntries(Object.entries(process.env) 17 | .filter(([key]) => key.startsWith('DBT_PROFILE_') && 18 | !CREDENTIAL_KEYS.includes(key.replace('DBT_PROFILE_', ''))) 19 | .map(([key, value]) => [ 20 | key.replace('DBT_PROFILE_', '').toLowerCase(), 21 | Number(value) || value, 22 | ])); 23 | const credentials = Object.fromEntries(Object.entries(process.env) 24 | .filter(([key]) => key.startsWith('DBT_PROFILE_') && 25 | CREDENTIAL_KEYS.includes(key.replace('DBT_PROFILE_', ''))) 26 | .map(([key, value]) => [ 27 | key.replace('DBT_PROFILE_', '').toLowerCase(), 28 | value, 29 | ])); 30 | return Object.keys(profileVariables).length > 0 31 | ? Object.assign(Object.assign({}, profileVariables), { type: profileVariables.type, credentials: Object.assign({}, credentials) }) : undefined; 32 | }; 33 | export default getProfileVariablesFromEnv; 34 | //# sourceMappingURL=getProfileVariablesFromEnvironment.js.map -------------------------------------------------------------------------------- /server/src/services/MetricService/utils/getProfileVariablesFromEnvironment.ts: -------------------------------------------------------------------------------- 1 | import {DbtProfile, Warehouse} from '../DbtLocalMetricService.js'; 2 | 3 | const getProfileVariablesFromEnv = (): DbtProfile | undefined => { 4 | const CREDENTIAL_KEYS = [ 5 | 'USER', 6 | 'PASSWORD', 7 | 'TYPE', 8 | 'PROJECT_ID', 9 | 'PRIVATE_KEY_ID', 10 | 'PRIVATE_KEY', 11 | 'CLIENT_EMAIL', 12 | 'CLIENT_ID', 13 | 'AUTH_URI', 14 | 'TOKEN_URI', 15 | 'AUTH_PROVIDER_X509_CERT_URL', 16 | 'CLIENT_X509_CERT_URL', 17 | ]; 18 | 19 | const profileVariables = Object.fromEntries( 20 | Object.entries(process.env) 21 | .filter( 22 | ([key]) => 23 | key.startsWith('DBT_PROFILE_') && 24 | !CREDENTIAL_KEYS.includes(key.replace('DBT_PROFILE_', '')) 25 | ) 26 | .map(([key, value]) => [ 27 | key.replace('DBT_PROFILE_', '').toLowerCase(), 28 | Number(value) || value, 29 | ]) 30 | ); 31 | 32 | const credentials = Object.fromEntries( 33 | Object.entries(process.env) 34 | .filter( 35 | ([key]) => 36 | key.startsWith('DBT_PROFILE_') && 37 | CREDENTIAL_KEYS.includes(key.replace('DBT_PROFILE_', '')) 38 | ) 39 | .map(([key, value]) => [ 40 | key.replace('DBT_PROFILE_', '').toLowerCase(), 41 | value, 42 | ]) 43 | ) as Record; 44 | 45 | return Object.keys(profileVariables).length > 0 46 | ? { 47 | ...profileVariables, 48 | type: profileVariables.type as Warehouse, 49 | credentials: {...credentials}, 50 | } 51 | : undefined; 52 | }; 53 | 54 | export default getProfileVariablesFromEnv; 55 | -------------------------------------------------------------------------------- /server/dist/services/dbtService.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"dbtService.js","sourceRoot":"","sources":["../../src/services/dbtService.ts"],"names":[],"mappings":";;;AAAA,iDAAuC;AAoBhC,MAAM,WAAW,GAAG,CAAC,IAAa,EAAE,YAAuB,EAAE,EAAE,EAAE;IACtE,OAAO,CAAC,KAAK,CACX,kCAAkC,IAAI,CAAC,SAAS,CAAC,EAAC,IAAI,EAAE,SAAS,EAAC,CAAC,EAAE,CACtE,CAAC;IACF,MAAM,EAAC,IAAI,EAAE,KAAK,EAAE,YAAY,EAAC,GAAG,SAAS,CAAC;IAE9C,8GAA8G;IAC9G,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,oBAAoB,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACzE,IAAI,OAAO,GAAG,IAAI,CAAC,KAAK,CACtB,GAAG;QACD,IAAA,wBAAQ,EACN,MAAM,OAAO,CAAC,GAAG,CAAC,gBAAgB;;;YAG9B,MAAM,EAAE,EACZ,EAAC,QAAQ,EAAE,OAAO,EAAC,CACpB;aACE,OAAO,EAAE;aACT,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;QACtB,GAAG,CACW,CAAC;IACnB,IAAI,IAAI,EAAE;QACR,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;KAC1D;IACD,IAAI,KAAK,EAAE;QACT,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;KAC5D;IACD,IAAI,YAAY,EAAE;QAChB,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,YAAY,CAAC,CAAC;KAC1E;IACD,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AA/BW,QAAA,WAAW,eA+BtB;AAYK,MAAM,WAAW,GAAG,CAAC,MAAmB,EAAU,EAAE;IACzD,OAAO,CAAC,KAAK,CAAC,kCAAkC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1E,MAAM,EACJ,WAAW,EACX,KAAK,EACL,UAAU,EACV,UAAU,EACV,QAAQ,EACR,MAAM,GAAG,MAAM,GAChB,GAAG,MAAM,CAAC;IAEX,MAAM,UAAU,GAAG,IAAA,wBAAQ,EACzB,MAAM,OAAO,CAAC,GAAG,CAAC,gBAAgB;uCAE1B,OAAO,CAAC,GAAG,CAAC,UACd,uCAAuC,IAAI,CAAC,SAAS,CAAC;QAC1D,WAAW;QACX,KAAK;QACL,UAAU;QACV,UAAU;QACV,QAAQ;QACR,MAAM;KACP,CAAC;OACC,EACH,EAAC,QAAQ,EAAE,OAAO,EAAC,CACpB,CAAC;IACF,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1B,MAAM,YAAY,GAAG,oBAAoB,CAAC;IAC1C,OAAO,UAAU,CAAC,KAAK,CACrB,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC,MAAM,CACvD,CAAC;AACJ,CAAC,CAAC;AA/BW,QAAA,WAAW,eA+BtB"} -------------------------------------------------------------------------------- /server/dist/services/MetricService/DbtLocalMetricService.d.ts: -------------------------------------------------------------------------------- 1 | import { DBTResource, MetricService, QueryParams, Selectors } from './types/index.js'; 2 | interface DbtMetricService extends MetricService { 3 | installMetricsPackage: () => void; 4 | listMetrics: (name?: string, selectors?: Selectors) => DBTResource[]; 5 | queryMetric: (params: QueryParams) => Record; 6 | } 7 | export declare enum Warehouse { 8 | BIGQUERY = "bigquery", 9 | POSTGRES = "postgres", 10 | REDSHIFT = "redshift", 11 | SNOWFLAKE = "snowflake" 12 | } 13 | declare type Credentials = Record; 14 | interface BigqueryProfile { 15 | type: Warehouse.BIGQUERY; 16 | credentials: Credentials; 17 | } 18 | interface PostgresProfile { 19 | type: Warehouse.POSTGRES; 20 | credentials: Credentials; 21 | } 22 | interface RedshiftProfile { 23 | type: Warehouse.REDSHIFT; 24 | credentials: Credentials; 25 | } 26 | interface SnowflakeProfile { 27 | type: Warehouse.SNOWFLAKE; 28 | credentials: Credentials; 29 | } 30 | export declare type DbtProfile = BigqueryProfile | PostgresProfile | RedshiftProfile | SnowflakeProfile; 31 | export default class DbtLocalMetricService implements DbtMetricService { 32 | private dbtProjectPath; 33 | private dbtProfilePath?; 34 | private profile?; 35 | private credentials?; 36 | private target?; 37 | constructor(props: { 38 | dbtProjectPath?: string; 39 | target?: string; 40 | profile?: string; 41 | profileVariables?: DbtProfile; 42 | }); 43 | installMetricsPackage: () => void; 44 | listMetrics: (name?: string | undefined, selectors?: Selectors) => DBTResource[]; 45 | queryMetric: (params: QueryParams) => Record; 46 | } 47 | export {}; 48 | -------------------------------------------------------------------------------- /server/dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,OAAO,eAAe,CAAC;AACvB,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,UAAU,MAAM,aAAa,CAAC;AACrC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAC,KAAK,EAAC,MAAM,oBAAoB,CAAC;AACzC,OAAO,aAAa,MAAM,0BAA0B,CAAC;AACrD,OAAO,OAAO,EAAE,EAAC,WAAW,EAAC,MAAM,qBAAqB,CAAC;AACzD,OAAO,OAAO,MAAM,qBAAqB,CAAC;AAC1C,OAAO,aAAa,MAAM,mCAAmC,CAAC;AAE9D,2BAA2B;AAC3B,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AAEtB,+CAA+C;AAC/C,GAAG,CAAC,GAAG,CACL,MAAM,CAAC;IACL,qBAAqB,EACnB,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK;CAC5D,CAAC,CACH,CAAC;AAEF,wDAAwD;AACxD,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;AAE3B,iCAAiC;AACjC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AAEhB,qCAAqC;AACrC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;AAE5B,2BAA2B;AAC3B,MAAM,KAAK,GACT,OAAO,CAAC,GAAG,CAAC,eAAe;IAC3B,IAAI,KAAK,CAAC;QACR,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe;QACrC,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB;QAC7C,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;QAC/D,OAAO,EACL,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,MAAM;YAC9B,CAAC,CAAC,uBAAuB;YACzB,CAAC,CAAC,uBAAuB;QAC7B,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,MAAM;QACvC,oBAAoB,EAAE,IAAI;KAC3B,CAAC,CAAC;AAEL,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;AAErC,yDAAyD;AACzD,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE;IACjC,MAAM,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACzD,aAAa,CAAC,qBAAqB,EAAE,CAAC;CACvC;AAED,WAAW,EAAE,CAAC;AACd,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAC7B,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAE7B,sBAAsB;AACtB,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,GAAG,CAAC,IAAI,mCAAI,IAAI,CAAC;AACtC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,EAAE,CAAC,CAAC;AAC3C,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import bodyParser from 'body-parser'; 4 | import cors from 'cors'; 5 | import helmet from 'helmet'; 6 | import morgan from 'morgan'; 7 | import {Kable} from 'kable-node-express'; 8 | import githubService from './services/gitService.js'; 9 | import graphql, {graphqlInit} from './routes/graphql.js'; 10 | import metrics from './routes/metrics.js'; 11 | import metricService from './services/MetricService/index.js'; 12 | 13 | // defining the Express app 14 | const app = express(); 15 | 16 | // adding Helmet to enhance your API's security 17 | app.use( 18 | helmet({ 19 | contentSecurityPolicy: 20 | process.env.NODE_ENV === 'production' ? undefined : false, 21 | }) 22 | ); 23 | 24 | // using bodyParser to parse JSON bodies into JS objects 25 | app.use(bodyParser.json()); 26 | 27 | // enabling CORS for all requests 28 | app.use(cors()); 29 | 30 | // adding morgan to log HTTP requests 31 | app.use(morgan('combined')); 32 | 33 | // Kable for authentication 34 | const kable = 35 | process.env.KABLE_CLIENT_ID && 36 | new Kable({ 37 | clientId: process.env.KABLE_CLIENT_ID, 38 | clientSecret: process.env.KABLE_CLIENT_SECRET, 39 | environment: process.env.KABLE_ENV === 'LIVE' ? 'LIVE' : 'TEST', 40 | baseUrl: 41 | process.env.KABLE_ENV === 'LIVE' 42 | ? 'https://live.kable.io' 43 | : 'https://test.kable.io', 44 | debug: process.env.KABLE_ENV === 'LIVE', 45 | recordAuthentication: true, 46 | }); 47 | 48 | kable && app.use(kable.authenticate); 49 | 50 | // Copy and initialize the dbt repo from Github if needed 51 | if (process.env.GITHUB_REPOSITORY) { 52 | await githubService.clone(process.env.GITHUB_REPOSITORY); 53 | metricService.installMetricsPackage(); 54 | } 55 | 56 | graphqlInit(); 57 | app.use('/metrics', metrics); 58 | app.use('/graphql', graphql); 59 | 60 | // starting the server 61 | const port = process.env.PORT ?? 3001; 62 | app.listen(port, () => { 63 | console.log(`listening on port ${port}`); 64 | }); 65 | -------------------------------------------------------------------------------- /server/dist/index.js: -------------------------------------------------------------------------------- 1 | var _a; 2 | import 'dotenv/config'; 3 | import express from 'express'; 4 | import bodyParser from 'body-parser'; 5 | import cors from 'cors'; 6 | import helmet from 'helmet'; 7 | import morgan from 'morgan'; 8 | import { Kable } from 'kable-node-express'; 9 | import githubService from './services/gitService.js'; 10 | import graphql, { graphqlInit } from './routes/graphql.js'; 11 | import metrics from './routes/metrics.js'; 12 | import metricService from './services/MetricService/index.js'; 13 | // defining the Express app 14 | const app = express(); 15 | // adding Helmet to enhance your API's security 16 | app.use(helmet({ 17 | contentSecurityPolicy: process.env.NODE_ENV === 'production' ? undefined : false, 18 | })); 19 | // using bodyParser to parse JSON bodies into JS objects 20 | app.use(bodyParser.json()); 21 | // enabling CORS for all requests 22 | app.use(cors()); 23 | // adding morgan to log HTTP requests 24 | app.use(morgan('combined')); 25 | // Kable for authentication 26 | const kable = process.env.KABLE_CLIENT_ID && 27 | new Kable({ 28 | clientId: process.env.KABLE_CLIENT_ID, 29 | clientSecret: process.env.KABLE_CLIENT_SECRET, 30 | environment: process.env.KABLE_ENV === 'LIVE' ? 'LIVE' : 'TEST', 31 | baseUrl: process.env.KABLE_ENV === 'LIVE' 32 | ? 'https://live.kable.io' 33 | : 'https://test.kable.io', 34 | debug: process.env.KABLE_ENV === 'LIVE', 35 | recordAuthentication: true, 36 | }); 37 | kable && app.use(kable.authenticate); 38 | // Copy and initialize the dbt repo from Github if needed 39 | if (process.env.GITHUB_REPOSITORY) { 40 | await githubService.clone(process.env.GITHUB_REPOSITORY); 41 | metricService.installMetricsPackage(); 42 | } 43 | graphqlInit(); 44 | app.use('/metrics', metrics); 45 | app.use('/graphql', graphql); 46 | // starting the server 47 | const port = (_a = process.env.PORT) !== null && _a !== void 0 ? _a : 3001; 48 | app.listen(port, () => { 49 | console.log(`listening on port ${port}`); 50 | }); 51 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /server/src/routes/metrics.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import metricService from '../services/MetricService/index.js'; 3 | 4 | const router = express.Router(); 5 | 6 | /* Lists all available metrics */ 7 | router.get('/', (req, res) => { 8 | res.type('application/json'); 9 | const {name, type, model, package_name} = req.query as Record; 10 | try { 11 | const output = JSON.stringify( 12 | metricService.listMetrics(name, {type, model, package_name}) 13 | ); 14 | res.send(output); 15 | } catch (error) { 16 | console.error(error); 17 | res.status(404).send(error); 18 | } 19 | }); 20 | 21 | /* Gets a metric's information */ 22 | router.get('/:name', (req, res) => { 23 | const {name} = req.params; 24 | try { 25 | const [metric] = metricService.listMetrics(name); 26 | const output = JSON.stringify(metric); 27 | res.send(output); 28 | } catch (error) { 29 | console.error(error); 30 | res.status(404).send(error); 31 | } 32 | }); 33 | 34 | /* Runs a given metric */ 35 | router.post('/:metric_name', (req, res) => { 36 | const {metric_name} = req.params; 37 | const {grain, dimensions, start_date, end_date} = req.body; 38 | 39 | let format: 'csv' | 'json'; 40 | switch (req.accepts(['json', 'csv'])) { 41 | case 'csv': 42 | format = 'csv'; 43 | res.type('text/csv'); 44 | break; 45 | default: 46 | format = 'json'; 47 | res.type('application/json'); 48 | } 49 | 50 | if (!metric_name) { 51 | res.status(400).send({ 52 | error: 'metric_name is a required property; no metric_name given', 53 | }); 54 | } 55 | if (!grain) { 56 | res 57 | .status(400) 58 | .send({error: 'grain is a required property; no grain given'}); 59 | } 60 | try { 61 | const output = metricService.queryMetric({ 62 | metric_name, 63 | grain, 64 | dimensions, 65 | start_date, 66 | end_date, 67 | format, 68 | }); 69 | res.send(output); 70 | } catch (error) { 71 | console.error(error); 72 | res.status(404).send(error); 73 | } 74 | }); 75 | 76 | export default router; 77 | -------------------------------------------------------------------------------- /server/dist/routes/metrics.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/routes/metrics.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /server/dist/routes/metrics.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import metricService from '../services/MetricService/index.js'; 3 | const router = express.Router(); 4 | /* Lists all available metrics */ 5 | router.get('/', (req, res) => { 6 | res.type('application/json'); 7 | const { name, type, model, package_name } = req.query; 8 | try { 9 | const output = JSON.stringify(metricService.listMetrics(name, { type, model, package_name })); 10 | res.send(output); 11 | } 12 | catch (error) { 13 | console.error(error); 14 | res.status(404).send(error); 15 | } 16 | }); 17 | /* Gets a metric's information */ 18 | router.get('/:name', (req, res) => { 19 | const { name } = req.params; 20 | try { 21 | const [metric] = metricService.listMetrics(name); 22 | const output = JSON.stringify(metric); 23 | res.send(output); 24 | } 25 | catch (error) { 26 | console.error(error); 27 | res.status(404).send(error); 28 | } 29 | }); 30 | /* Runs a given metric */ 31 | router.post('/:metric_name', (req, res) => { 32 | const { metric_name } = req.params; 33 | const { grain, dimensions, start_date, end_date } = req.body; 34 | let format; 35 | switch (req.accepts(['json', 'csv'])) { 36 | case 'csv': 37 | format = 'csv'; 38 | res.type('text/csv'); 39 | break; 40 | default: 41 | format = 'json'; 42 | res.type('application/json'); 43 | } 44 | if (!metric_name) { 45 | res.status(400).send({ 46 | error: 'metric_name is a required property; no metric_name given', 47 | }); 48 | } 49 | if (!grain) { 50 | res 51 | .status(400) 52 | .send({ error: 'grain is a required property; no grain given' }); 53 | } 54 | try { 55 | const output = metricService.queryMetric({ 56 | metric_name, 57 | grain, 58 | dimensions, 59 | start_date, 60 | end_date, 61 | format, 62 | }); 63 | res.send(output); 64 | } 65 | catch (error) { 66 | console.error(error); 67 | res.status(404).send(error); 68 | } 69 | }); 70 | export default router; 71 | //# sourceMappingURL=metrics.js.map -------------------------------------------------------------------------------- /server/dist/services/dbtService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.queryMetric = exports.listMetrics = void 0; 4 | const child_process_1 = require("child_process"); 5 | const listMetrics = (name, selectors = {}) => { 6 | console.debug(`called listMetrics with params ${JSON.stringify({ name, selectors })}`); 7 | const { type, model, package_name } = selectors; 8 | // TODO: added some basic replacement to prevent bash injection, but I should clean this up here and elsewhere 9 | const select = name ? `--select "metric:${name.replace(/"/g, '')}"` : ''; 10 | let metrics = JSON.parse('[' + 11 | (0, child_process_1.execSync)(`cd ${process.env.DBT_PROJECT_PATH} &&\ 12 | dbt ls --resource-type metric --output json \ 13 | --output-keys "name model label description type time_grains dimensions filters unique_id package_name" \ 14 | ${select}`, { encoding: 'utf-8' }) 15 | .trimEnd() 16 | .replace(/\n/g, ',') + 17 | ']'); 18 | if (type) { 19 | metrics = metrics.filter(metric => metric.type === type); 20 | } 21 | if (model) { 22 | metrics = metrics.filter(metric => metric.model === model); 23 | } 24 | if (package_name) { 25 | metrics = metrics.filter(metric => metric.package_name === package_name); 26 | } 27 | return metrics; 28 | }; 29 | exports.listMetrics = listMetrics; 30 | const queryMetric = (params) => { 31 | console.debug(`called queryMetric with params ${JSON.stringify(params)}`); 32 | const { metric_name, grain, dimensions, start_date, end_date, format = 'json', } = params; 33 | const raw_output = (0, child_process_1.execSync)(`cd ${process.env.DBT_PROJECT_PATH} &&\ 34 | dbt run-operation --target ${process.env.DBT_TARGET} dbt_metrics_api.run_metric --args '${JSON.stringify({ 35 | metric_name, 36 | grain, 37 | dimensions, 38 | start_date, 39 | end_date, 40 | format, 41 | })}' 42 | `, { encoding: 'utf-8' }); 43 | console.debug(raw_output); 44 | const BREAK_STRING = '<<>>\n'; 45 | return raw_output.slice(raw_output.indexOf(BREAK_STRING) + BREAK_STRING.length); 46 | }; 47 | exports.queryMetric = queryMetric; 48 | //# sourceMappingURL=dbtService.js.map -------------------------------------------------------------------------------- /server/dist/services/metricService.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"metricService.js","sourceRoot":"","sources":["../../src/services/metricService.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /server/dist/services/gitService.js: -------------------------------------------------------------------------------- 1 | var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { 2 | if (kind === "m") throw new TypeError("Private method is not writable"); 3 | if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); 4 | if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); 5 | return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; 6 | }; 7 | var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { 8 | if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); 9 | if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); 10 | return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); 11 | }; 12 | var _GithubService_accessToken, _GithubService_client; 13 | import simpleGit from 'simple-git'; 14 | // eslint-disable-next-line node/no-extraneous-import 15 | import tempy from 'tempy'; 16 | export class GithubService { 17 | constructor(accessToken) { 18 | _GithubService_accessToken.set(this, void 0); 19 | _GithubService_client.set(this, void 0); 20 | if (!accessToken) { 21 | console.debug('no access token provided. initializing blank GithubService instance'); 22 | } 23 | __classPrivateFieldSet(this, _GithubService_accessToken, accessToken, "f"); 24 | __classPrivateFieldSet(this, _GithubService_client, simpleGit(), "f"); 25 | this.dir = accessToken ? tempy.directory({ prefix: '_github' }) : undefined; 26 | } 27 | async clone(repository) { 28 | if (!__classPrivateFieldGet(this, _GithubService_accessToken, "f") || !this.dir) { 29 | throw Error('Cannot clone, no access token was provided in the environment'); 30 | } 31 | const url = `https://${__classPrivateFieldGet(this, _GithubService_accessToken, "f")}@github.com/${repository}.git`; 32 | await __classPrivateFieldGet(this, _GithubService_client, "f").env('GIT_TERMINAL_PROMPT', '0').clone(url, this.dir); 33 | } 34 | } 35 | _GithubService_accessToken = new WeakMap(), _GithubService_client = new WeakMap(); 36 | const instance = new GithubService(process.env.GITHUB_ACCESS_TOKEN); 37 | export default instance; 38 | //# sourceMappingURL=gitService.js.map -------------------------------------------------------------------------------- /server/dist/services/metricService.js: -------------------------------------------------------------------------------- 1 | import { execFileSync, execSync } from 'child_process'; 2 | import gitService from './gitService.js'; 3 | const DBT_PROJECT_PATH = gitService.dir || process.env.DBT_PROJECT_PATH; 4 | console.info(DBT_PROJECT_PATH); 5 | export const installMetricsPackage = () => { 6 | console.debug('called installMetricsPackage'); 7 | execSync('echo -e "\n - git: https://github.com/mjirv/dbt-metrics-api.git\n revision: main" >> packages.yml', { cwd: DBT_PROJECT_PATH, shell: 'bash' }); 8 | execFileSync('dbt', ['deps'], { cwd: DBT_PROJECT_PATH }); 9 | }; 10 | export const listMetrics = (name, selectors = {}) => { 11 | var _a; 12 | console.debug(`called listMetrics with params ${JSON.stringify({ name, selectors })}`); 13 | const { type, model, package_name } = selectors; 14 | const select = name ? `--select "metric:${name.replace(/"/g, '')}"` : ''; 15 | const res = '[' + 16 | ((_a = execFileSync('dbt', [ 17 | 'ls', 18 | '--resource-type', 19 | 'metric', 20 | '--output', 21 | 'json', 22 | '--output-keys', 23 | '"name model label description type time_grains dimensions filters unique_id package_name"', 24 | ...(select ? [select] : []), 25 | ], { cwd: DBT_PROJECT_PATH }) 26 | .toString() 27 | .trimEnd() 28 | .match(/\{.*\}/i)) === null || _a === void 0 ? void 0 : _a.toString().replace(/\n/g, ',')) + 29 | ']'; 30 | console.info(res); 31 | let metrics = JSON.parse(res); 32 | if (type) { 33 | metrics = metrics.filter(metric => metric.type === type); 34 | } 35 | if (model) { 36 | metrics = metrics.filter(metric => metric.model === model); 37 | } 38 | if (package_name) { 39 | metrics = metrics.filter(metric => metric.package_name === package_name); 40 | } 41 | return metrics; 42 | }; 43 | export const queryMetric = (params) => { 44 | console.debug(`called queryMetric with params ${JSON.stringify(params)}`); 45 | const { metric_name, grain, dimensions, start_date, end_date, format = 'json', } = params; 46 | const raw_output = execFileSync('dbt', [ 47 | 'run-operation', 48 | ...(process.env.DBT_TARGET ? ['--target', process.env.DBT_TARGET] : []), 49 | 'dbt_metrics_api.run_metric', 50 | '--args', 51 | `${JSON.stringify({ 52 | metric_name, 53 | grain, 54 | dimensions, 55 | start_date, 56 | end_date, 57 | format, 58 | })}`, 59 | ], { cwd: DBT_PROJECT_PATH, encoding: 'utf-8' }).toString(); 60 | console.debug(raw_output); 61 | const BREAK_STRING = '<<>>\n'; 62 | return raw_output.slice(raw_output.indexOf(BREAK_STRING) + BREAK_STRING.length); 63 | }; 64 | //# sourceMappingURL=metricService.js.map -------------------------------------------------------------------------------- /server/dist/routes/graphql.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"graphql.js","sourceRoot":"","sources":["../../src/routes/graphql.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /server/dist/routes/graphql.js: -------------------------------------------------------------------------------- 1 | /* GraphQL methods */ 2 | import { graphqlHTTP } from 'express-graphql'; 3 | import { GraphQLEnumType, GraphQLFloat, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, } from 'graphql'; 4 | import metricService from '../services/MetricService/index.js'; 5 | import express from 'express'; 6 | const router = express.Router(); 7 | const refreshSchema = (_req, res) => { 8 | graphqlInit(); 9 | res.status(200).end(); 10 | }; 11 | let graphqlMiddleware = (req, res, next) => { 12 | var _a; 13 | if ((_a = graphqlInit()) === null || _a === void 0 ? void 0 : _a.success) { 14 | graphqlMiddleware(req, res, next); 15 | } 16 | next(); 17 | }; 18 | export function graphqlInit() { 19 | const metricToGraphQLType = (metric) => new GraphQLObjectType({ 20 | name: metric.name, 21 | fields: Object.assign({ date_day: { type: GraphQLString }, date_week: { type: GraphQLString }, date_month: { type: GraphQLString }, date_quarter: { type: GraphQLString }, date_year: { type: GraphQLString }, [metric.name]: { type: GraphQLFloat } }, Object.fromEntries(metric.dimensions.map(dimension => [dimension, { type: GraphQLString }]) // TODO: they might be other things 22 | )), 23 | }); 24 | let availableMetrics = []; 25 | try { 26 | availableMetrics = metricService.listMetrics(); 27 | } 28 | catch (error) { 29 | console.warn(error); 30 | } 31 | const GrainType = new GraphQLEnumType({ name: "Grain", values: Object.fromEntries(["day", "week", "month", "quarter", "year"].map(grain => [grain, { value: grain }])) }); 32 | const QueryType = new GraphQLObjectType({ 33 | name: 'Query', 34 | fields: Object.assign({}, Object.fromEntries(availableMetrics.map(metric => [ 35 | metric.name, 36 | { 37 | type: new GraphQLList(metricToGraphQLType(metric)), 38 | args: { 39 | grain: { 40 | type: new GraphQLNonNull(GrainType) 41 | }, 42 | start_date: { type: GraphQLString }, 43 | end_date: { type: GraphQLString }, 44 | }, 45 | }, 46 | ]))), 47 | }); 48 | const schema = new GraphQLSchema({ 49 | query: QueryType, 50 | }); 51 | function metricResolver(args, _context, { fieldName, fieldNodes }) { 52 | var _a; 53 | const NON_DIMENSION_FIELDS = [fieldName, 'date_day', 'date_week', 'date_month', 'date_quarter', 'date_year']; 54 | const [node] = fieldNodes; 55 | const res = metricService.queryMetric(Object.assign({ metric_name: fieldName, dimensions: (_a = node.selectionSet) === null || _a === void 0 ? void 0 : _a.selections.map(selection => selection.name.value).filter(field => !NON_DIMENSION_FIELDS.includes(field)) }, args)); 56 | console.info(res); 57 | return res; 58 | } 59 | const root = availableMetrics.reduce((prev, current) => { 60 | return Object.assign(Object.assign({}, prev), { [current.name]: metricResolver }); 61 | }, {}); 62 | if (Object.keys(root).length > 0) { 63 | graphqlMiddleware = graphqlHTTP({ 64 | schema: schema, 65 | rootValue: root, 66 | graphiql: true, 67 | }); 68 | return { success: true }; 69 | } 70 | return { success: false }; 71 | } 72 | router.use('/', (req, res, next) => graphqlMiddleware(req, res, next)); 73 | router.post('/refresh', refreshSchema); 74 | export default router; 75 | //# sourceMappingURL=graphql.js.map -------------------------------------------------------------------------------- /server/src/routes/graphql.ts: -------------------------------------------------------------------------------- 1 | /* GraphQL methods */ 2 | 3 | import {graphqlHTTP} from 'express-graphql'; 4 | import { 5 | FieldNode, 6 | GraphQLEnumType, 7 | GraphQLFloat, 8 | GraphQLList, 9 | GraphQLNonNull, 10 | GraphQLObjectType, 11 | GraphQLScalarType, 12 | GraphQLSchema, 13 | GraphQLString, 14 | GraphQLUnionType, 15 | printSchema, 16 | } from 'graphql'; 17 | import metricService from '../services/MetricService/index.js'; 18 | import type {DBTResource, Grain} from '../services/MetricService/index.js'; 19 | import express, {NextFunction, Request, Response} from 'express'; 20 | 21 | const router = express.Router(); 22 | 23 | const refreshSchema = (_req: Request, res: Response) => { 24 | graphqlInit(); 25 | res.status(200).end(); 26 | }; 27 | 28 | let graphqlMiddleware = (req: Request, res: Response, next: NextFunction) => { 29 | if (graphqlInit()?.success) { 30 | graphqlMiddleware(req, res, next); 31 | } 32 | next(); 33 | }; 34 | 35 | export function graphqlInit() { 36 | const metricToGraphQLType = (metric: DBTResource) => 37 | new GraphQLObjectType({ 38 | name: metric.name, 39 | fields: { 40 | date_day: {type: GraphQLString}, // TODO: should this be date? 41 | date_week: {type: GraphQLString}, 42 | date_month: {type: GraphQLString}, 43 | date_quarter: {type: GraphQLString}, 44 | date_year: {type: GraphQLString}, 45 | [metric.name]: {type: GraphQLFloat}, 46 | // eslint-disable-next-line node/no-unsupported-features/es-builtins 47 | ...Object.fromEntries( 48 | metric.dimensions.map(dimension => [dimension, {type: GraphQLString}]) // TODO: they might be other things 49 | ), 50 | }, 51 | }); 52 | 53 | let availableMetrics: DBTResource[] = []; 54 | 55 | try { 56 | availableMetrics = metricService.listMetrics(); 57 | } catch (error) { 58 | console.warn(error); 59 | } 60 | 61 | const GrainType = new GraphQLEnumType({ name: "Grain", values: Object.fromEntries(["day", "week", "month", "quarter", "year"].map(grain => [grain, { value: grain }])) }) 62 | 63 | const QueryType = new GraphQLObjectType({ 64 | name: 'Query', 65 | fields: { 66 | ...Object.fromEntries( 67 | availableMetrics.map(metric => [ 68 | metric.name, 69 | { 70 | type: new GraphQLList(metricToGraphQLType(metric)), 71 | args: { 72 | grain: { 73 | type: new GraphQLNonNull(GrainType) 74 | }, 75 | start_date: {type: GraphQLString}, 76 | end_date: {type: GraphQLString}, 77 | }, 78 | }, 79 | ]) 80 | ), 81 | }, 82 | }); 83 | 84 | const schema = new GraphQLSchema({ 85 | query: QueryType, 86 | }); 87 | 88 | interface MetricArgs { 89 | grain: Grain; 90 | start_date?: string; 91 | end_date?: string; 92 | } 93 | 94 | function metricResolver( 95 | args: MetricArgs, 96 | _context: never, 97 | {fieldName, fieldNodes}: {fieldName: string; fieldNodes: FieldNode[]} 98 | ) { 99 | const NON_DIMENSION_FIELDS = [fieldName, 'date_day', 'date_week', 'date_month', 'date_quarter', 'date_year']; 100 | const [node] = fieldNodes; 101 | const res = metricService.queryMetric({ 102 | metric_name: fieldName, 103 | dimensions: node.selectionSet?.selections 104 | .map(selection => (selection as FieldNode).name.value) 105 | .filter(field => !NON_DIMENSION_FIELDS.includes(field)), 106 | ...args, 107 | }); 108 | console.info(res); 109 | return res; 110 | } 111 | 112 | const root = availableMetrics.reduce((prev, current) => { 113 | return {...prev, [current.name]: metricResolver}; 114 | }, {}); 115 | 116 | if (Object.keys(root).length > 0) { 117 | graphqlMiddleware = graphqlHTTP({ 118 | schema: schema, 119 | rootValue: root, 120 | graphiql: true, 121 | }); 122 | return {success: true}; 123 | } 124 | return {success: false}; 125 | } 126 | 127 | router.use('/', (req, res, next) => graphqlMiddleware(req, res, next)); 128 | router.post('/refresh', refreshSchema); 129 | 130 | export default router; 131 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/DbtLocalMetricService.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"DbtLocalMetricService.js","sourceRoot":"","sources":["../../../src/services/MetricService/DbtLocalMetricService.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,eAAe,CAAC;AAO3C,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,KAAK,MAAM,OAAO,CAAC;AAQ1B,MAAM,CAAN,IAAY,SAKX;AALD,WAAY,SAAS;IACnB,kCAAqB,CAAA;IACrB,kCAAqB,CAAA;IACrB,kCAAqB,CAAA;IACrB,oCAAuB,CAAA;AACzB,CAAC,EALW,SAAS,KAAT,SAAS,QAKpB;AAkCD,MAAM,CAAC,OAAO,OAAO,qBAAqB;IAMxC,YAAY,KAKX;QAgDD,0BAAqB,GAAG,GAAG,EAAE;YAC3B,MAAM,iBAAiB,GAAG,GAAG,IAAI,CAAC,cAAc,eAAe,CAAC;YAChE,MAAM,mBAAmB,GAAG;gBAC1B,GAAG,EAAE,8CAA8C;gBACnD,QAAQ,EAAE,MAAM;aACjB,CAAC;YAEF,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YAE9C,MAAM,EAAC,QAAQ,EAAC,GAAG,IAAI,CAAC,IAAI,CAC1B,EAAE,CAAC,YAAY,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAC7B,CAAC;YACjB,IAAI,CAAC,CAAA,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,mBAAmB,CAAC,GAAG,CAAC,CAAA,EAAE;gBAC7D,OAAO,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;gBACxD,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;gBACnC,EAAE,CAAC,aAAa,CAAC,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,EAAC,QAAQ,EAAC,CAAC,CAAC,CAAC;aAC5D;YAED,IAAI;gBACF,YAAY,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,EAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAC,CAAC,CAAC;aAC3D;YAAC,OAAO,KAAK,EAAE;gBACd,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACrB,MAAM,KAAK,CAAC;aACb;QACH,CAAC,CAAC;QAEF,gBAAW,GAAG,CAAC,IAAa,EAAE,YAAuB,EAAE,EAAE,EAAE;;YACzD,OAAO,CAAC,KAAK,CACX,kCAAkC,IAAI,CAAC,SAAS,CAAC,EAAC,IAAI,EAAE,SAAS,EAAC,CAAC,EAAE,CACtE,CAAC;YACF,MAAM,EAAC,IAAI,EAAE,KAAK,EAAE,YAAY,EAAC,GAAG,SAAS,CAAC;YAE9C,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,oBAAoB,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACzE,MAAM,GAAG,GACP,GAAG;iBACH,MAAA,YAAY,CACV,KAAK,EACL;oBACE,IAAI;oBACJ,iBAAiB;oBACjB,QAAQ;oBACR,UAAU;oBACV,MAAM;oBACN,eAAe;oBACf,2FAA2F;oBAC3F,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC3B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBACpD,GAAG,CAAC,IAAI,CAAC,cAAc;wBACrB,CAAC,CAAC,CAAC,gBAAgB,EAAE,IAAI,CAAC,cAAc,CAAC;wBACzC,CAAC,CAAC,EAAE,CAAC;iBACR,EACD;oBACE,GAAG,EAAE,IAAI,CAAC,cAAc;oBACxB,QAAQ,EAAE,OAAO;oBACjB,GAAG,kCAAM,OAAO,CAAC,GAAG,GAAK,IAAI,CAAC,WAAW,CAAC;iBAC3C,CACF;qBACE,OAAO,EAAE;qBACT,KAAK,CAAC,SAAS,CAAC,0CACf,QAAQ,EAAE,CAAA;;oBAEd,GAAG,CAAC;YAEN,IAAI,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAC;YAC/C,IAAI,IAAI,EAAE;gBACR,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;aAC1D;YACD,IAAI,KAAK,EAAE;gBACT,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;aAC5D;YACD,IAAI,YAAY,EAAE;gBAChB,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,YAAY,CAAC,CAAC;aAC1E;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC;QAEF,gBAAW,GAAG,CAAC,MAAmB,EAAmC,EAAE;YACrE,OAAO,CAAC,KAAK,CAAC,kCAAkC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC1E,MAAM,EACJ,WAAW,EACX,KAAK,EACL,UAAU,EACV,UAAU,EACV,QAAQ,EACR,MAAM,GAAG,MAAM,GAChB,GAAG,MAAM,CAAC;YAEX,IAAI;gBACF,MAAM,UAAU,GAAG,YAAY,CAC7B,KAAK,EACL;oBACE,eAAe;oBACf,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBACjD,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBACpD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,gBAAgB,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBACvE,4BAA4B;oBAC5B,QAAQ;oBACR,GAAG,IAAI,CAAC,SAAS,CAAC;wBAChB,WAAW;wBACX,KAAK;wBACL,UAAU;wBACV,UAAU;wBACV,QAAQ;wBACR,MAAM;qBACP,CAAC,EAAE;iBACL,EACD;oBACE,GAAG,EAAE,IAAI,CAAC,cAAc;oBACxB,QAAQ,EAAE,OAAO;oBACjB,GAAG,kCAAM,OAAO,CAAC,GAAG,GAAK,IAAI,CAAC,WAAW,CAAC;iBAC3C,CACF,CAAC,QAAQ,EAAE,CAAC;gBACb,MAAM,YAAY,GAAG,oBAAoB,CAAC;gBAC1C,OAAO,IAAI,CAAC,KAAK,CACf,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CACzE,CAAC;aACH;YACD,OAAO,KAAK,EAAE;gBACZ,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,KAAK,CAAC,CAAA;gBACjE,MAAM,IAAI,KAAK,CAAE,KAA4B,CAAC,MAAM,CAAC,CAAA;aACtD;QAEH,CAAC,CAAC;QAzKA,MAAM,EAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAC,GAAG,KAAK,CAAC;QAClE,IAAI,CAAC,cAAc;YAAE,MAAM,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAC9D,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;QACrC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,WAAW,GAAG,gBAAgB,aAAhB,gBAAgB,uBAAhB,gBAAgB,CAAE,WAAW,CAAC;QACjD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QAEvB,IAAI,gBAAgB,EAAE;YACpB,IAAI,CAAC,OAAO,GAAG,cAAc,CAAC;YAC9B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,MAAM,EAAC,WAAW,KAA8B,gBAAgB,EAAzC,qBAAqB,UAAI,gBAAgB,EAA1D,eAAuC,CAAmB,CAAC;YACjE,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,oBAAoB,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;YACxE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,SAAS,CAAC,EAAC,MAAM,EAAE,cAAc,EAAC,CAAC,CAAC;YAChE,OAAO,CAAC,KAAK,CACX,uEAAuE,IAAI,CAAC,cAAc,EAAE,CAC7F,CAAC;YACF,MAAM,kBAAkB,GAAG,MAAM,CAAC,WAAW,CAC3C,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClC,GAAG;gBACH,eAAe,MAAM,CAAC,GAAG,CAAC,OAAO;aAClC,CAAC,CACH,CAAC;YACF,MAAM,cAAc,GAAG;gBACrB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;oBACd,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,OAAO,EAAE;wBACP,CAAC,IAAI,CAAC,MAAM,CAAC,kCACR,qBAAqB,GACrB,CAAC,gBAAgB,CAAC,IAAI,KAAK,SAAS,CAAC,QAAQ;4BAChD,WAAW,CAAC,MAAM,KAAK,sBAAsB;4BAC3C,CAAC,CAAC,EAAC,WAAW,EAAE,kBAAkB,EAAC;4BACnC,CAAC,CAAC,kBAAkB,CAAC,CACxB;qBACF;iBACF;aACF,CAAC;YACF,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CACnC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAC5D,CAAC;YACF,EAAE,CAAC,aAAa,CACd,GAAG,IAAI,CAAC,cAAc,eAAe,EACrC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAC1B,CAAC;YACF,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;SACjD;IACH,CAAC;CA6HF"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demeter 2 | 3 | ## The dbt Metrics API 4 | 5 | Demeter turns your dbt project into a metrics platform. You get a REST/GraphQL API so you can query, build interactive apps, and use your metrics from anywhere. 6 | 7 | [View live demo](https://dbt-demeter-demo.herokuapp.com/graphql) 8 | 9 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 10 | 11 | - [Demeter](#demeter) 12 | - [The dbt Metrics API](#the-dbt-metrics-api) 13 | - [About](#about) 14 | - [Installation](#installation) 15 | - [Prerequisites](#prerequisites) 16 | - [Quickstart (local)](#quickstart-local) 17 | - [Usage](#usage) 18 | - [Routes](#routes) 19 | - [GET `/metrics`](#get-metrics) 20 | - [GET `/metrics/:metric_name`](#get-metricsmetric_name) 21 | - [POST `/metrics/:metric_name`](#post-metricsmetric_name) 22 | - [POST `/graphql`](#post-graphql) 23 | - [Advanced](#advanced) 24 | - [Running a production instance](#running-a-production-instance) 25 | - [Authentication](#authentication) 26 | 27 | ## About 28 | 29 | Demeter gives you a REST API (including a GraphQL endpoint) to query your dbt metrics 30 | 31 | ![graphql](https://user-images.githubusercontent.com/5953854/158102577-f935b647-88f4-4180-b161-81f86a454ccb.PNG) 32 | 33 | ## Installation 34 | 35 | ### Prerequisites 36 | 37 | 1. A [dbt project](https://docs.getdbt.com/tutorial/setting-up) with [metrics](https://github.com/dbt-labs/dbt_metrics) 38 | 2. Node 39 | - Run `node --version` 40 | - If there is no output or node is not found, follow the steps at https://heynode.com/tutorial/install-nodejs-locally-nvm/ to install Node. 41 | 42 | ### Quickstart (local) 43 | 44 | 1. **📦 Install dbt-metrics-api** 45 | - Add the following to your dbt project's `packages.yml` file: 46 | ```yaml 47 | - git: "https://github.com/mjirv/demeter.git" 48 | revision: main 49 | ``` 50 | - Run `dbt deps` 51 | 2. **✅ Run** 52 | - Run `cd dbt_packages/dbt_metrics_api/server && npm i && node dist/` 53 | - Navigate to http://localhost:3001/graphql in your browser to start querying your API! 54 | 55 | ## Usage 56 | 57 | ### Routes 58 | 59 | dbt-metrics-api has 4 routes: 60 | 61 | #### GET `/metrics` 62 | 63 | - returns a JSON array of your project's metrics 64 | - supports "name", "type", "model", and "package_name" query strings for filtering 65 | 66 | $ curl -X GET "http://localhost:3001/metrics?type=count&package_name=jaffle_shop" 67 | 68 | [{"unique_id":"metric.jaffle_shop.orders","package_name":"jaffle_shop","model":"ref('orders')","name":"orders","description":"The number of orders","label":"Orders","type":"count","filters":[],"time_grains":["day","week","month","quarter","year"],"dimensions":["status","customer_id"]},{"unique_id":"metric.jaffle_shop.orders2","package_name":"jaffle_shop","model":"ref('orders')","name":"orders2","description":"The number of orders","label":"Orders","type":"count","filters":[],"time_grains":["day","week","month","quarter","year"],"dimensions":["status","customer_id"]}] 69 | 70 | #### GET `/metrics/:metric_name` 71 | 72 | - returns a JSON object with keys `unique_id, package_name, model, name, description, label, type, filters, time_grains, dimensions` 73 | 74 | $ curl -X GET "http://localhost:3001/metrics/orders" 75 | 76 | {"unique_id":"metric.jaffle_shop.orders","package_name":"jaffle_shop","model":"ref('orders')","name":"orders","description":"The number of orders","label":"Orders","type":"count","filters":[],"time_grains":["day","week","month","quarter","year"],"dimensions":["status","customer_id"]} 77 | 78 | #### POST `/metrics/:metric_name` 79 | 80 | - Queries a metric and returns the result 81 | - Accepts a JSON object in the request body with the following properties: `grain, dimensions, start_date, end_date` (`start_date` and `end_date` are optional) 82 | - Returns a JSON object or CSV depending on your `Accept:` header (`application/json` or `text/csv`) 83 | 84 | $ curl http://localhost:3001/metrics/orders -H "Content-Type: application/json" -H "Accept: application/json" -d '{"grain": "year", "start_date": "2017-01-01", "end_date": "2019-01-01"}' 85 | 86 | [{"period": "2017-01-01", "orders": 0.0}, {"period": "2018-01-01", "orders": 99.0}, {"period": "2019-01-01", "orders": 0.0}] 87 | 88 | #### POST `/graphql` 89 | 90 | - GraphQL API for your metrics 91 | - Visit `YOUR_SERVER_PATH/graphql` (e.g. http://localhost:3001/graphql) to see the GraphiQL client and schema 92 | 93 | ## Advanced 94 | 95 | ### Running a production instance 96 | 97 | On your favorite cloud hosting service (e.g. Heroku or GCP Cloud Run) install this repo via the GitHub URL. 98 | 99 | Look in `.example.env` for the available environment variables and configure them as needed. 100 | 101 | ### Authentication 102 | 103 | dbt-metrics-api supports [Kable](https://kable.io) for authentication. 104 | 105 | To get started: 106 | 107 | 1. Sign up at https://kable.io (the free tier includes unlimited test requests and up to 10,000 live requests) 108 | 2. Note your client ID and secret and add them to your `server/.env.local` file: 109 | 110 | ```bash 111 | KABLE_CLIENT_ID= 112 | KABLE_CLIENT_SECRET= 113 | 114 | # you will need to set to LIVE in production 115 | KABLE_ENV=TEST 116 | ``` 117 | 118 | 3. Requests will now require authentication with a customer ID and secret that you set up in your Kable dashboard using `X-CLIENT-ID` and `X-API-KEY` headers 119 | 120 | ```json 121 | $ curl -X GET "http://localhost:3001/metrics/orders" -H "X-CLIENT-ID: test-customer-1" -H "X-API-KEY: sk_test.some.secret.key" 122 | ``` 123 | -------------------------------------------------------------------------------- /server/dist/services/MetricService/DbtLocalMetricService.js: -------------------------------------------------------------------------------- 1 | var __rest = (this && this.__rest) || function (s, e) { 2 | var t = {}; 3 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 4 | t[p] = s[p]; 5 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 6 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 7 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 8 | t[p[i]] = s[p[i]]; 9 | } 10 | return t; 11 | }; 12 | import { execFileSync } from 'child_process'; 13 | import fs from 'fs'; 14 | import yaml from 'js-yaml'; 15 | import tempy from 'tempy'; 16 | export var Warehouse; 17 | (function (Warehouse) { 18 | Warehouse["BIGQUERY"] = "bigquery"; 19 | Warehouse["POSTGRES"] = "postgres"; 20 | Warehouse["REDSHIFT"] = "redshift"; 21 | Warehouse["SNOWFLAKE"] = "snowflake"; 22 | })(Warehouse || (Warehouse = {})); 23 | export default class DbtLocalMetricService { 24 | constructor(props) { 25 | this.installMetricsPackage = () => { 26 | const PACKAGE_YAML_PATH = `${this.dbtProjectPath}/packages.yml`; 27 | const METRICS_API_PACKAGE = { 28 | git: 'https://github.com/mjirv/dbt-metrics-api.git', 29 | revision: 'main', 30 | }; 31 | console.debug('called installMetricsPackage'); 32 | const { packages } = yaml.load(fs.readFileSync(PACKAGE_YAML_PATH, 'utf-8')); 33 | if (!(packages === null || packages === void 0 ? void 0 : packages.find(el => el.git === METRICS_API_PACKAGE.git))) { 34 | console.debug('adding metrics package to packages.yml'); 35 | packages.push(METRICS_API_PACKAGE); 36 | fs.writeFileSync(PACKAGE_YAML_PATH, yaml.dump({ packages })); 37 | } 38 | try { 39 | execFileSync('dbt', ['deps'], { cwd: this.dbtProjectPath }); 40 | } 41 | catch (error) { 42 | console.error(error); 43 | throw error; 44 | } 45 | }; 46 | this.listMetrics = (name, selectors = {}) => { 47 | var _a; 48 | console.debug(`called listMetrics with params ${JSON.stringify({ name, selectors })}`); 49 | const { type, model, package_name } = selectors; 50 | const select = name ? `--select "metric:${name.replace(/"/g, '')}"` : ''; 51 | const res = '[' + 52 | ((_a = execFileSync('dbt', [ 53 | 'ls', 54 | '--resource-type', 55 | 'metric', 56 | '--output', 57 | 'json', 58 | '--output-keys', 59 | '"name model label description type time_grains dimensions filters unique_id package_name"', 60 | ...(select ? [select] : []), 61 | ...(this.profile ? ['--profile', this.profile] : []), 62 | ...(this.dbtProfilePath 63 | ? ['--profiles-dir', this.dbtProfilePath] 64 | : []), 65 | ], { 66 | cwd: this.dbtProjectPath, 67 | encoding: 'utf-8', 68 | env: Object.assign(Object.assign({}, process.env), this.credentials), 69 | }) 70 | .trimEnd() 71 | .match(/\{.*\}/g)) === null || _a === void 0 ? void 0 : _a.toString()) 72 | + 73 | ']'; 74 | let metrics = JSON.parse(res); 75 | if (type) { 76 | metrics = metrics.filter(metric => metric.type === type); 77 | } 78 | if (model) { 79 | metrics = metrics.filter(metric => metric.model === model); 80 | } 81 | if (package_name) { 82 | metrics = metrics.filter(metric => metric.package_name === package_name); 83 | } 84 | return metrics; 85 | }; 86 | this.queryMetric = (params) => { 87 | console.debug(`called queryMetric with params ${JSON.stringify(params)}`); 88 | const { metric_name, grain, dimensions, start_date, end_date, format = 'json', } = params; 89 | try { 90 | const raw_output = execFileSync('dbt', [ 91 | 'run-operation', 92 | ...(this.target ? ['--target', this.target] : []), 93 | ...(this.profile ? ['--profile', this.profile] : []), 94 | ...(this.dbtProfilePath ? ['--profiles-dir', this.dbtProfilePath] : []), 95 | 'dbt_metrics_api.run_metric', 96 | '--args', 97 | `${JSON.stringify({ 98 | metric_name, 99 | grain, 100 | dimensions, 101 | start_date, 102 | end_date, 103 | format, 104 | })}`, 105 | ], { 106 | cwd: this.dbtProjectPath, 107 | encoding: 'utf-8', 108 | env: Object.assign(Object.assign({}, process.env), this.credentials), 109 | }).toString(); 110 | const BREAK_STRING = '<<>>\n'; 111 | return JSON.parse(raw_output.slice(raw_output.indexOf(BREAK_STRING) + BREAK_STRING.length)); 112 | } 113 | catch (error) { 114 | console.error("An error occurred while querying a metric", error); 115 | throw new Error(error.stdout); 116 | } 117 | }; 118 | const { dbtProjectPath, target, profile, profileVariables } = props; 119 | if (!dbtProjectPath) 120 | throw Error('no dbt project path given'); 121 | this.dbtProjectPath = dbtProjectPath; 122 | this.target = target; 123 | this.credentials = profileVariables === null || profileVariables === void 0 ? void 0 : profileVariables.credentials; 124 | this.profile = profile; 125 | if (profileVariables) { 126 | this.profile = 'mapi_profile'; 127 | this.target = 'prod'; 128 | const { credentials } = profileVariables, profileWithoutSecrets = __rest(profileVariables, ["credentials"]); 129 | const envVar = (key) => `MAPI_DBT_PROFILE_${key.toUpperCase()}`; 130 | this.dbtProfilePath = tempy.directory({ prefix: '_dbt_profile' }); 131 | console.debug(`profileVariables found; beginning to write profile.yml to directory ${this.dbtProfilePath}`); 132 | const credentialsToWrite = Object.fromEntries(Object.keys(credentials).map(key => [ 133 | key, 134 | `{{ env_var('${envVar(key)}') }}`, 135 | ])); 136 | const profileToWrite = { 137 | [this.profile]: { 138 | target: this.target, 139 | outputs: { 140 | [this.target]: Object.assign(Object.assign({}, profileWithoutSecrets), (profileVariables.type === Warehouse.BIGQUERY && 141 | credentials.method === 'service-account-json' 142 | ? { keyfileJson: credentialsToWrite } 143 | : credentialsToWrite)), 144 | }, 145 | }, 146 | }; 147 | this.credentials = Object.fromEntries(Object.entries(credentials).map(([k, v]) => [envVar(k), v])); 148 | fs.writeFileSync(`${this.dbtProfilePath}/profiles.yml`, yaml.dump(profileToWrite)); 149 | console.debug('successfully wrote profile.yml'); 150 | } 151 | } 152 | } 153 | //# sourceMappingURL=DbtLocalMetricService.js.map -------------------------------------------------------------------------------- /server/src/services/MetricService/DbtLocalMetricService.ts: -------------------------------------------------------------------------------- 1 | import {execFileSync} from 'child_process'; 2 | import { 3 | DBTResource, 4 | MetricService, 5 | QueryParams, 6 | Selectors, 7 | } from './types/index.js'; 8 | import fs from 'fs'; 9 | import yaml from 'js-yaml'; 10 | import tempy from 'tempy'; 11 | 12 | interface DbtMetricService extends MetricService { 13 | installMetricsPackage: () => void; 14 | listMetrics: (name?: string, selectors?: Selectors) => DBTResource[]; 15 | queryMetric: (params: QueryParams) => Record; 16 | } 17 | 18 | export enum Warehouse { 19 | BIGQUERY = 'bigquery', 20 | POSTGRES = 'postgres', 21 | REDSHIFT = 'redshift', 22 | SNOWFLAKE = 'snowflake', 23 | } 24 | 25 | type Credentials = Record; 26 | 27 | interface BigqueryProfile { 28 | type: Warehouse.BIGQUERY; 29 | credentials: Credentials; 30 | } 31 | 32 | interface PostgresProfile { 33 | type: Warehouse.POSTGRES; 34 | credentials: Credentials; 35 | } 36 | 37 | interface RedshiftProfile { 38 | type: Warehouse.REDSHIFT; 39 | credentials: Credentials; 40 | } 41 | 42 | interface SnowflakeProfile { 43 | type: Warehouse.SNOWFLAKE; 44 | credentials: Credentials; 45 | } 46 | 47 | export type DbtProfile = 48 | | BigqueryProfile 49 | | PostgresProfile 50 | | RedshiftProfile 51 | | SnowflakeProfile; 52 | 53 | interface PackageYaml { 54 | packages: Record[]; 55 | } 56 | 57 | export default class DbtLocalMetricService implements DbtMetricService { 58 | private dbtProjectPath: string; 59 | private dbtProfilePath?: string; 60 | private profile?: string; 61 | private credentials?: Record; 62 | private target?: string; 63 | constructor(props: { 64 | dbtProjectPath?: string; 65 | target?: string; 66 | profile?: string; 67 | profileVariables?: DbtProfile; 68 | }) { 69 | const {dbtProjectPath, target, profile, profileVariables} = props; 70 | if (!dbtProjectPath) throw Error('no dbt project path given'); 71 | this.dbtProjectPath = dbtProjectPath; 72 | this.target = target; 73 | this.credentials = profileVariables?.credentials; 74 | this.profile = profile; 75 | 76 | if (profileVariables) { 77 | this.profile = 'mapi_profile'; 78 | this.target = 'prod'; 79 | const {credentials, ...profileWithoutSecrets} = profileVariables; 80 | const envVar = (key: string) => `MAPI_DBT_PROFILE_${key.toUpperCase()}`; 81 | this.dbtProfilePath = tempy.directory({prefix: '_dbt_profile'}); 82 | console.debug( 83 | `profileVariables found; beginning to write profile.yml to directory ${this.dbtProfilePath}` 84 | ); 85 | const credentialsToWrite = Object.fromEntries( 86 | Object.keys(credentials).map(key => [ 87 | key, 88 | `{{ env_var('${envVar(key)}') }}`, 89 | ]) 90 | ); 91 | const profileToWrite = { 92 | [this.profile]: { 93 | target: this.target, 94 | outputs: { 95 | [this.target]: { 96 | ...profileWithoutSecrets, 97 | ...(profileVariables.type === Warehouse.BIGQUERY && 98 | credentials.method === 'service-account-json' 99 | ? {keyfileJson: credentialsToWrite} 100 | : credentialsToWrite), 101 | }, 102 | }, 103 | }, 104 | }; 105 | this.credentials = Object.fromEntries( 106 | Object.entries(credentials).map(([k, v]) => [envVar(k), v]) 107 | ); 108 | fs.writeFileSync( 109 | `${this.dbtProfilePath}/profiles.yml`, 110 | yaml.dump(profileToWrite) 111 | ); 112 | console.debug('successfully wrote profile.yml'); 113 | } 114 | } 115 | 116 | installMetricsPackage = () => { 117 | const PACKAGE_YAML_PATH = `${this.dbtProjectPath}/packages.yml`; 118 | const METRICS_API_PACKAGE = { 119 | git: 'https://github.com/mjirv/dbt-metrics-api.git', 120 | revision: 'main', 121 | }; 122 | 123 | console.debug('called installMetricsPackage'); 124 | 125 | const {packages} = yaml.load( 126 | fs.readFileSync(PACKAGE_YAML_PATH, 'utf-8') 127 | ) as PackageYaml; 128 | if (!packages?.find(el => el.git === METRICS_API_PACKAGE.git)) { 129 | console.debug('adding metrics package to packages.yml'); 130 | packages.push(METRICS_API_PACKAGE); 131 | fs.writeFileSync(PACKAGE_YAML_PATH, yaml.dump({packages})); 132 | } 133 | 134 | try { 135 | execFileSync('dbt', ['deps'], {cwd: this.dbtProjectPath}); 136 | } catch (error) { 137 | console.error(error); 138 | throw error; 139 | } 140 | }; 141 | 142 | listMetrics = (name?: string, selectors: Selectors = {}) => { 143 | console.debug( 144 | `called listMetrics with params ${JSON.stringify({name, selectors})}` 145 | ); 146 | const {type, model, package_name} = selectors; 147 | 148 | const select = name ? `--select "metric:${name.replace(/"/g, '')}"` : ''; 149 | const res = 150 | '[' + 151 | execFileSync( 152 | 'dbt', 153 | [ 154 | 'ls', 155 | '--resource-type', 156 | 'metric', 157 | '--output', 158 | 'json', 159 | '--output-keys', 160 | '"name model label description type time_grains dimensions filters unique_id package_name"', 161 | ...(select ? [select] : []), 162 | ...(this.profile ? ['--profile', this.profile] : []), 163 | ...(this.dbtProfilePath 164 | ? ['--profiles-dir', this.dbtProfilePath] 165 | : []), 166 | ], 167 | { 168 | cwd: this.dbtProjectPath, 169 | encoding: 'utf-8', 170 | env: {...process.env, ...this.credentials}, 171 | } 172 | ) 173 | .trimEnd() 174 | .match(/\{.*\}/g) 175 | ?.toString() 176 | + 177 | ']'; 178 | 179 | let metrics = JSON.parse(res) as DBTResource[]; 180 | if (type) { 181 | metrics = metrics.filter(metric => metric.type === type); 182 | } 183 | if (model) { 184 | metrics = metrics.filter(metric => metric.model === model); 185 | } 186 | if (package_name) { 187 | metrics = metrics.filter(metric => metric.package_name === package_name); 188 | } 189 | return metrics; 190 | }; 191 | 192 | queryMetric = (params: QueryParams): Record => { 193 | console.debug(`called queryMetric with params ${JSON.stringify(params)}`); 194 | const { 195 | metric_name, 196 | grain, 197 | dimensions, 198 | start_date, 199 | end_date, 200 | format = 'json', 201 | } = params; 202 | 203 | try { 204 | const raw_output = execFileSync( 205 | 'dbt', 206 | [ 207 | 'run-operation', 208 | ...(this.target ? ['--target', this.target] : []), 209 | ...(this.profile ? ['--profile', this.profile] : []), 210 | ...(this.dbtProfilePath ? ['--profiles-dir', this.dbtProfilePath] : []), 211 | 'dbt_metrics_api.run_metric', 212 | '--args', 213 | `${JSON.stringify({ 214 | metric_name, 215 | grain, 216 | dimensions, 217 | start_date, 218 | end_date, 219 | format, 220 | })}`, 221 | ], 222 | { 223 | cwd: this.dbtProjectPath, 224 | encoding: 'utf-8', 225 | env: {...process.env, ...this.credentials}, 226 | } 227 | ).toString(); 228 | const BREAK_STRING = '<<>>\n'; 229 | return JSON.parse( 230 | raw_output.slice(raw_output.indexOf(BREAK_STRING) + BREAK_STRING.length) 231 | ); 232 | } 233 | catch (error) { 234 | console.error("An error occurred while querying a metric", error) 235 | throw new Error((error as { stdout: string }).stdout) 236 | } 237 | 238 | }; 239 | } 240 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Michael J. Irvine 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------