Automatically generate Typescript types for v2 API query schema (#4574)

* Generate types from query schema

* Flip the query schema so private is static

* Ensure private schema stays private

* Refactor comment, json schema utils
This commit is contained in:
Artur Pata
2024-09-18 14:01:20 +03:00
committed by GitHub
parent 7a77ebf9bf
commit 82a15884ad
10 changed files with 719 additions and 58 deletions

View File

@ -26,6 +26,7 @@ jobs:
node-version: ${{steps.versions.outputs.nodejs}}
- run: npm install --prefix ./assets
- run: npm install --prefix ./tracker
- run: npm run generate-types --prefix ./assets && git diff --exit-code -- ./assets/js/types
- run: npm run typecheck --prefix ./assets
- run: npm run lint --prefix ./assets
- run: npm run check-format --prefix ./assets

157
assets/js/types/query-api.d.ts vendored Normal file
View File

@ -0,0 +1,157 @@
/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/
export type Metric =
| "time_on_page"
| "visitors"
| "visits"
| "pageviews"
| "views_per_visit"
| "bounce_rate"
| "visit_duration"
| "events"
| "percentage"
| "conversion_rate"
| "group_conversion_rate";
export type DateRangeShorthand = "30m" | "realtime" | "all" | "day" | "7d" | "30d" | "month" | "6mo" | "12mo" | "year";
/**
* @minItems 2
* @maxItems 2
*/
export type DateTimeRange = [string, string];
/**
* @minItems 2
* @maxItems 2
*/
export type DateRange = [string, string];
export type Dimensions = SimpleFilterDimensions | CustomPropertyFilterDimensions | GoalDimension | TimeDimensions;
export type SimpleFilterDimensions =
| "event:name"
| "event:page"
| "event:hostname"
| "visit:source"
| "visit:channel"
| "visit:referrer"
| "visit:utm_medium"
| "visit:utm_source"
| "visit:utm_campaign"
| "visit:utm_content"
| "visit:utm_term"
| "visit:screen"
| "visit:device"
| "visit:browser"
| "visit:browser_version"
| "visit:os"
| "visit:os_version"
| "visit:country"
| "visit:region"
| "visit:city"
| "visit:country_name"
| "visit:region_name"
| "visit:city_name"
| "visit:entry_page"
| "visit:exit_page"
| "visit:entry_page_hostname"
| "visit:exit_page_hostname";
export type CustomPropertyFilterDimensions = string;
export type GoalDimension = "event:goal";
export type TimeDimensions = "time" | "time:month" | "time:week" | "time:day" | "time:hour";
export type FilterTree = FilterEntry | FilterAndOr | FilterNot;
export type FilterEntry = FilterWithoutGoals | FilterWithGoals;
/**
* @minItems 3
* @maxItems 3
*/
export type FilterWithoutGoals = [
FilterOperationWithoutGoals | ("matches_wildcard" | "matches_wildcard_not"),
SimpleFilterDimensions | CustomPropertyFilterDimensions,
Clauses
];
/**
* filter operation
*/
export type FilterOperationWithoutGoals = "is_not" | "contains_not" | "matches" | "matches_not";
export type Clauses = (string | number)[];
/**
* @minItems 3
* @maxItems 3
*/
export type FilterWithGoals = [
FilterOperationWithGoals,
GoalDimension | SimpleFilterDimensions | CustomPropertyFilterDimensions,
Clauses
];
/**
* filter operation
*/
export type FilterOperationWithGoals = "is" | "contains";
/**
* @minItems 2
* @maxItems 2
*/
export type FilterAndOr = ["and" | "or", [FilterTree, ...FilterTree[]]];
/**
* @minItems 2
* @maxItems 2
*/
export type FilterNot = ["not", FilterTree];
/**
* @minItems 2
* @maxItems 2
*/
export type OrderByEntry = [
Metric | SimpleFilterDimensions | CustomPropertyFilterDimensions | TimeDimensions,
"asc" | "desc"
];
export interface QueryApiSchema {
/**
* Domain of site to query
*/
site_id: string;
/**
* List of metrics to query
*
* @minItems 1
*/
metrics: [Metric, ...Metric[]];
date?: string;
/**
* Date range to query
*/
date_range: DateRangeShorthand | DateTimeRange | DateRange;
/**
* What to group the results by. Same as `property` in Plausible API v1
*/
dimensions?: Dimensions[];
/**
* How to drill into your data
*/
filters?: FilterTree[];
/**
* How to order query results
*/
order_by?: OrderByEntry[];
include?: {
time_labels?: boolean;
imports?: boolean;
/**
* If set, returns the total number of result rows rows before pagination under `meta.total_rows`
*/
total_rows?: boolean;
};
pagination?: {
/**
* Number of rows to limit result to.
*/
limit?: number;
/**
* Pagination offset.
*/
offset?: number;
};
}

370
assets/package-lock.json generated
View File

@ -65,6 +65,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"json-schema-to-typescript": "^15.0.2",
"prettier": "^3.3.3",
"stylelint": "^16.8.1",
"stylelint-config-standard": "^36.0.1",
@ -102,6 +103,23 @@
"node": ">=6.0.0"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.7.0",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.0.tgz",
"integrity": "sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog==",
"dev": true,
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@babel/code-frame": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
@ -782,6 +800,96 @@
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -1580,6 +1688,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true
},
"node_modules/@jsonurl/jsonurl": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@jsonurl/jsonurl/-/jsonurl-1.1.7.tgz",
@ -1618,6 +1732,16 @@
"node": ">= 8"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.6",
"license": "MIT",
@ -2355,12 +2479,24 @@
"parse5": "^7.0.0"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
"dev": true
},
"node_modules/@types/node": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.2.0.tgz",
@ -4441,6 +4577,12 @@
"node": ">=12"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@ -5454,6 +5596,22 @@
"is-callable": "^1.1.3"
}
},
"node_modules/foreground-child": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@ -6538,6 +6696,21 @@
"set-function-name": "^2.0.1"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jake": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
@ -8311,6 +8484,73 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"node_modules/json-schema-to-typescript": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.2.tgz",
"integrity": "sha512-+cRBw+bBJ3k783mZroDIgz1pLNPB4hvj6nnbHTWwEVl0dkW8qdZ+M9jWhBb+Y0FAdHvNsXACga3lewGO8lktrw==",
"dev": true,
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.5.5",
"@types/json-schema": "^7.0.15",
"@types/lodash": "^4.17.7",
"glob": "^10.3.12",
"is-glob": "^4.0.3",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"prettier": "^3.2.5"
},
"bin": {
"json2ts": "dist/src/cli.js"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/json-schema-to-typescript/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/json-schema-to-typescript/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/json-schema-to-typescript/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -8661,9 +8901,22 @@
}
},
"node_modules/minimist": {
"version": "1.2.6",
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT"
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/ms": {
"version": "2.1.2",
@ -8948,6 +9201,12 @@
"node": ">=6"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"dev": true
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -9018,6 +9277,28 @@
"version": "1.0.7",
"license": "MIT"
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@ -10068,6 +10349,27 @@
"node": ">=8"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -10180,6 +10482,19 @@
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@ -11218,6 +11533,57 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi-cjs/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",

View File

@ -9,7 +9,8 @@
"eslint": "eslint js/**",
"stylelint": "stylelint css/**",
"lint": "npm run eslint && npm run stylelint",
"typecheck": "tsc --noEmit --pretty"
"typecheck": "tsc --noEmit --pretty",
"generate-types": "json2ts ../priv/json-schemas/query-api-schema.json ../assets/js/types/query-api.d.ts"
},
"dependencies": {
"@headlessui/react": "^1.7.10",
@ -68,6 +69,7 @@
"eslint-plugin-react-hooks": "^4.6.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"json-schema-to-typescript": "^15.0.2",
"prettier": "^3.3.3",
"stylelint": "^16.8.1",
"stylelint-config-standard": "^36.0.1",

View File

@ -0,0 +1,44 @@
defmodule Plausible.Stats.JSONSchema.Utils do
@moduledoc """
Module for traversing and modifying JSON schemas.
"""
@type json :: map() | list() | String.t() | number() | boolean() | nil
@type transform_fun :: (json() -> json() | :remove)
@spec traverse(map(), transform_fun()) :: map() | :remove
def traverse(json, fun) when is_map(json) do
result =
Enum.reduce(json, %{}, fn {k, v}, acc ->
case traverse(v, fun) do
:remove -> acc
transformed_v -> Map.put(acc, k, transformed_v)
end
end)
case result do
map when map_size(map) == 0 -> fun.(%{})
map -> fun.(map)
end
end
@spec traverse(list(), transform_fun()) :: list() | :remove
def traverse(json, fun) when is_list(json) do
result =
Enum.reduce(json, [], fn v, acc ->
case traverse(v, fun) do
:remove -> acc
transformed_v -> [transformed_v | acc]
end
end)
|> Enum.reverse()
case result do
[] -> fun.([])
list -> fun.(list)
end
end
@spec traverse(String.t() | number() | boolean() | nil, transform_fun()) :: json() | :remove
def traverse(value, fun), do: fun.(value)
end

View File

@ -5,37 +5,20 @@ defmodule Plausible.Stats.JSONSchema do
Note that `internal` queries expose some metrics, filter types and other features not
available on the public API.
"""
alias Plausible.Stats.JSONSchema.Utils
@external_resource "priv/json-schemas/query-api-schema.json"
@raw_public_schema Application.app_dir(:plausible, "priv/json-schemas/query-api-schema.json")
|> File.read!()
|> Jason.decode!()
@raw_internal_schema Application.app_dir(:plausible, "priv/json-schemas/query-api-schema.json")
|> File.read!()
|> Jason.decode!()
@raw_public_schema Utils.traverse(@raw_internal_schema, fn
%{"$comment" => "only :internal"} -> :remove
value -> value
end)
@internal_query_schema ExJsonSchema.Schema.resolve(@raw_internal_schema)
@public_query_schema ExJsonSchema.Schema.resolve(@raw_public_schema)
@internal_query_schema @raw_public_schema
# Add overrides for things allowed in the internal API
|> JSONPointer.add!(
"#/definitions/filter_operation_without_goals/enum/0",
"matches_wildcard"
)
|> JSONPointer.add!(
"#/definitions/filter_operation_without_goals/enum/0",
"matches_wildcard_not"
)
|> JSONPointer.add!("#/definitions/metric/oneOf/0", %{
"const" => "time_on_page"
})
|> JSONPointer.add!("#/definitions/date_range/oneOf/0", %{
"const" => "30m"
})
|> JSONPointer.add!("#/definitions/date_range/oneOf/0", %{
"const" => "realtime"
})
|> JSONPointer.add!("#/properties/date", %{"type" => "string"})
|> ExJsonSchema.Schema.resolve()
def validate(schema_type, params) do
case ExJsonSchema.Validator.validate(schema(schema_type), params) do
:ok -> :ok

View File

@ -15,8 +15,16 @@
"uniqueItems": true,
"description": "List of metrics to query"
},
"date": {
"type": "string",
"$comment": "only :internal"
},
"date_range": {
"$ref": "#/definitions/date_range",
"oneOf": [
{ "$ref": "#/definitions/date_range_shorthand" },
{ "$ref": "#/definitions/date_time_range" },
{ "$ref": "#/definitions/date_range" }
],
"description": "Date range to query"
},
"dimensions": {
@ -41,6 +49,7 @@
},
"include": {
"type": "object",
"additionalProperties": false,
"properties": {
"time_labels": {
"type": "boolean",
@ -58,6 +67,7 @@
},
"pagination": {
"type": "object",
"additionalProperties": false,
"properties": {
"limit": {
"type": "integer",
@ -77,8 +87,52 @@
"required": ["site_id", "metrics", "date_range"],
"additionalProperties": false,
"definitions": {
"date_time_range": {
"type": "array",
"additionalItems": false,
"minItems": 2,
"maxItems": 2,
"markdownDescription": "A list of two ISO8601 datetimes.",
"examples": [["2024-01-01T00:00:00+03:00", "2024-01-02T12:00:00+03:00"]],
"items": [
{
"type": "string",
"format": "date-time"
},
{
"type": "string",
"format": "date-time"
}
]
},
"date_range": {
"type": "array",
"additionalItems": false,
"minItems": 2,
"maxItems": 2,
"markdownDescription": "A list of two ISO8601 dates.",
"examples": [["2024-09-01", "2024-09-30"]],
"items": [
{
"type": "string",
"format": "date"
},
{
"type": "string",
"format": "date"
}
]
},
"date_range_shorthand": {
"oneOf": [
{
"const": "30m",
"$comment": "only :internal"
},
{
"const": "realtime",
"$comment": "only :internal"
},
{
"const": "all",
"description": "Since the start of stats in Plausible"
@ -110,37 +164,15 @@
{
"const": "year",
"description": "Since the start of this year"
},
{
"type": "array",
"additionalItems": false,
"minItems": 2,
"maxItems": 2,
"items": {
"oneOf": [
{
"type": "string",
"format": "date"
},
{
"type": "string",
"format": "date-time"
}
]
},
"markdownDescription": "A list of two ISO8601 dates or timestamps to determine the query date range.",
"examples": [
["2024-01-01", "2024-01-31"],
[
"2024-01-01T00:00:00+03:00",
"2024-01-02T12:00:00+03:00"
]
]
}
]
},
"metric": {
"oneOf": [
{
"const": "time_on_page",
"$comment": "only :internal"
},
{
"const": "visitors",
"description": "Metric counting the number of unique visitors"
@ -243,6 +275,11 @@
"type": ["string", "integer"]
}
},
"filter_operation_wildcard": {
"type": "string",
"enum": ["matches_wildcard", "matches_wildcard_not"],
"description": "filter operation"
},
"filter_operation_without_goals": {
"type": "string",
"enum": ["is_not", "contains_not", "matches", "matches_not"],
@ -259,7 +296,15 @@
"minItems": 3,
"maxItems": 3,
"items": [
{ "$ref": "#/definitions/filter_operation_without_goals" },
{
"oneOf": [
{ "$ref": "#/definitions/filter_operation_without_goals" },
{
"$ref": "#/definitions/filter_operation_wildcard",
"$comment": "only :internal"
}
]
},
{
"oneOf": [
{ "$ref": "#/definitions/simple_filter_dimensions" },

View File

@ -0,0 +1,43 @@
defmodule Plausible.Stats.JSONSchema.UtilsTest do
use ExUnit.Case, async: true
alias Plausible.Stats.JSONSchema
describe "traversing" do
test "transform 'fn value -> value end' does not drop anything" do
json = %{foo: %{bar: [0, ""], baz: nil, pax: %{}}}
assert JSONSchema.Utils.traverse(json, fn value -> value end) == json
end
test "can remove specific items, keeping the original order of lists" do
assert JSONSchema.Utils.traverse(
%{
foo: [
"a",
%{type: "string", "$comment": "only :internal"},
%{type: "number"}
]
},
fn
%{"$comment": "only :internal"} -> :remove
value -> value
end
) == %{foo: ["a", %{type: "number"}]}
end
test "can transform specific items" do
assert JSONSchema.Utils.traverse(
%{
foo: [
%{type: "string", "$comment": "anything"},
%{type: "number"}
]
},
fn
%{"$comment": "anything"} -> %{type: "number", "$comment": "transformed"}
value -> value
end
) == %{foo: [%{type: "number", "$comment": "transformed"}, %{type: "number"}]}
end
end
end

View File

@ -896,7 +896,7 @@ defmodule Plausible.Stats.Filters.QueryParserTest do
}
|> check_error(
site,
"Invalid date_range '[\"2021-02-03T00:00:00Z\", \"2021-02-04\"]'."
"#/date_range: Invalid date range [\"2021-02-03T00:00:00Z\", \"2021-02-04\"]"
)
end

View File

@ -0,0 +1,20 @@
defmodule PlausibleWeb.Api.InternalController.SchemaForDocsTest do
use PlausibleWeb.ConnCase, async: true
use Plausible.Repo
describe "GET /api/docs/query/schema.json" do
test "returns public schema in json format and it parses", %{conn: conn} do
conn = get(conn, "/api/docs/query/schema.json")
response = json_response(conn, 200)
assert %{"$schema" => "http://json-schema.org/draft-07/schema#", "type" => "object"} =
response
end
test "public schema does not contain any unexpected nodes", %{conn: conn} do
conn = get(conn, "/api/docs/query/schema.json")
refute response(conn, 200) =~ ~s/"$comment":"only :internal"/
refute response(conn, 200) =~ ~s/"realtime"/
end
end
end