From 8fdb9133a22454ffa2d22fe6899294a1e7b47274 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Mon, 20 Sep 2021 20:24:10 -0400 Subject: [PATCH] Add Insights Module & API Aggregation Functionality (#8009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Apply aggregation query to dbQuery * Override fields/sortField when group/aggr is used * Sanitize incoming group/aggr fields * Validate for new group/aggr query params * Document new aggregate/group endpoint * Add changeset * Add new system tables * Add schema, rest/gql resolvers for insights * Add insights store * Render insights overview page * Add dashboard creation flow * Add not found route * Show editing grid * Add panels as extension type * Render panel selection * Add edit existing * Add saving changes * Add positioning * Finish resizing * Start on metric panel * Auto-expand workspace * WIP add frappe-chart * Add functional time-series chart * Deep watch option changes * Fix o2m fetch when not grouping * Allow PK in metric panel * Add breadcrumb * Various tweaks and fixes * Fix metric alignment, only load on options change, Show header * Add delete panel * Add updating dashboard * Swap docs / insights * Add sort/limit to metric * Add decimal places, units * Add label type panel * Track corner intersaction * Don't hit the API if there aren't any staged changes * Remove limit from metric * Extend resize handlers beyond border * Fix repositioning on update existing * Add duplicate panel * panel duplicate icon * Increase time series min height * Improve time series styling * make panels selectable * Button styling and fullscren (button only) * Time series color * Panel plot display * Optically align metric * Add number formatting to metric * Insights placeholders and defaults * Fix codemirror placeholder color * Restart docker containers on docker restart * Move insights to Vue 3 * Fix val check * Add button style props * Fix input/value * Fix panel init * Fix buttons on panels * Fix animation on panel config * Fix panel location not resetting on cancel * Add fullscreen / zoom to fit support * Temp remove transition to prevent browser glitches * Fix vertical size calculation * Fix panel editing * Update params to match fields * Setup datetime abstraction * Restructure fn helper * Add fields support for date functions * Allow functions in sort/filter * Fix missing knex passthrough * Finish date retrieval abstraction for all vendors * Delete witty-emus-approve.md * Delete dependabot.yml * Add renovate.json (#6322) Co-authored-by: Renovate Bot * New Crowdin updates (#6309) * New translations en-US.yaml (Japanese) * New translations en-US.yaml (Japanese) * New translations en-US.yaml (Arabic) * New translations en-US.yaml (Arabic) * New translations en-US.yaml (Arabic) * New translations en-US.yaml (Arabic) * fix link (#6339) * fix(deps): pin dependencies (#6323) Co-authored-by: Renovate Bot * chore(deps): update dependency globby to v11.0.4 (#6324) Co-authored-by: Renovate Bot * Add support for `read` hooks on `items` (#6341) * Add emitter on item read * Add performance warning to docs * Make result instead of query the payload * Redact tokens from logs (#6347) * Fixed issue that would cause uploads to the root folder of the file library to fail (#6348) fixes #6310 * Use existing file extension as default (#6349) * Don't send sensitive data in webhooks (#6350) Fixes #6246 * Trim val before check h/t @aidenfoxx * chore(deps): update mariadb docker tag to v10.6 (#6332) Co-authored-by: Renovate Bot * chore(deps): update node.js to v16 (#6336) Co-authored-by: Renovate Bot * chore(deps): update postgres docker tag to v13 (#6338) Co-authored-by: Renovate Bot * chore(deps): update dependency rollup to v2.52.1 (#6337) Co-authored-by: Renovate Bot * chore(deps): update dependency vue-router to v4.0.9 (#6327) Co-authored-by: Renovate Bot * chore(deps): update dependency typescript to v4.3.3 (#6329) Co-authored-by: Renovate Bot * fix(deps): update dependency ms to v2.1.3 (#6328) Co-authored-by: Renovate Bot * chore(deps): update dependency marked to v2.1.1 (#6330) Co-authored-by: Renovate Bot * chore(deps): update fullcalendar monorepo to v5.8.0 (#6331) Co-authored-by: Renovate Bot * chore(deps): update dependency dotenv to v10 (#6333) Co-authored-by: Renovate Bot * fix(deps): update dependency chalk to v4 (#6342) Co-authored-by: Renovate Bot * chore(deps): update dependency fs-extra to v10 (#6334) Co-authored-by: Renovate Bot * Change cache-control heeaders (#6355) * chore(deps): update dependency typescript to v4.3.4 (#6357) Co-authored-by: Renovate Bot * Fixed invalid onDelete constraint for some schemas (#6308) * Fixed invalid onDelete clause for some schemas * Ran prettier * Updated all onDelete statements to be Oracle friendly Co-authored-by: Aiden Foxx * Fix import in aggregation * Fix cancel button on new modal dialog * Add default icon to new dashboards * Add information sidebar component * Don't open sidebar on window resize * Add distinct options to metrics panel * Use updated aggregate function type signature * Reset field value on collection change * Don't show resize stats on edit click * Add panel options to header headline on drawer * Add page-bottom padding to drawers * Show panel icon in header, fix active state buttons * Add date range functionality to time-series * Fix z-index of edit buttons * Fix header icon color * Update insights module icon * Fix datetime formatter, set date range, add padding * Time series * tweaks on time series * format tweaks * Fix edit dashboard modal * Add auto-format option * Fix number formatting w/ decimals * Add metric conditional color * Fix defaults rendering in list, add defaults to metric * Fix decimal points in metrics * Remove sort * Tweaks in metrics settings * Add filters to time series * Update options for metric * Time series tweaks * Allow empty field for metric * Set label min height to 4 * Add first/last to metric * Add "move" option, various tweaks * Upgrade "move to" to "copy to" * Add white to color preset defaults * Tweaks * Use 0 for decimal default * Use default false for abbreviate * Fix panel registration * Show color placeholder, fix edit modal * Add navigation guard * Don't fire navguard on subroute * Show create button on empty dashboards in nav * Use synced charts * Undo sync test * Have metric render 0 * Fix abbreviate decimal places * Fix min 0 in time-series * less blocking whitespace * new metric min width * new time series min width * time series style updates * Fixed typo (#6558) * Fix auto-fill of directus_files in relational setup (#6555) Fixes #6487 * v9.0.0-rc.82 * Update changelog.md * Add limit options for deleteMany files (#6561) * Changed filesize to bigint for large files * Update api/src/database/migrations/20210626A-change-filesize-bigint.ts * add `limit -1` for deleteMany files options from #6560 Co-authored-by: Rijk van Zanten * Fix cleaning order * update dependency ts-node-dev to v1.1.7 (#6564) Co-authored-by: Renovate Bot * Fix order of form group filter (#6566) Fixes #6557 * New Crowdin updates (#6554) * New translations en-US.yaml (Bulgarian) * Update source file en-US.yaml * v9.0.0-rc.83 * Update the required Node version to 12.20.0 (#6578) * update dependency rollup to v2.52.4 (#6572) Co-authored-by: Renovate Bot * Add skip admin init flag (#6576) * adds skipAdminInit flag to bootstrap * checks for skipAdminInit flag * update docs for skipAdminInit * Fix extension loading on windows (#6579) Javascript import syntax uses URLs instead of paths, so we have to normalize the extension paths to forward slashes when importing them inside the virtual entrypoints. Fixes #6550 * New Crowdin updates (#6575) * New translations en-US.yaml (Hebrew) * New translations en-US.yaml (Hebrew) * insights time series min size * Only ask for are you sure when edits are made * Add cancel confirmation * Add system collections to pane dropdown * Disable zoom to fit when enabling edit mode * Render browser popup on reload * Fix padding in TV mode * Fix box * Add show X/Y axis options * Default to 0 decimals * Use configured decimals in Y axis labels * Fix build * Aggregate resolvers added to GraphQL options (#7373) * Don't use tags interface for CSV filter (#7258) Fixes #6778 * Rely on `RETURNING` when possible (#7259) * WIP use returning clause instead of max from id * Use returning where applicable, fallback to fetch Fixes #6279 * update dependency p-queue to v7 (#7255) Co-authored-by: Renovate Bot * update dependency @vitejs/plugin-vue to v1.4.0 (#7263) Co-authored-by: Renovate Bot * Move p-queue to app dev dependencies (#7273) * Log error message when registering app extension fails (#7274) * update dependency rollup to v2.56.1 (#7269) Co-authored-by: Renovate Bot * update dependency vue-router to v4.0.11 (#7272) Co-authored-by: Renovate Bot * update dependency ts-node to v10.2.0 (#7271) Co-authored-by: Renovate Bot * Only loads app extensions if SERVE_APP is true (#7275) This also ensures API/App only load their respective extensions in dev. * Fix gitignore file in extension templates being deleted when publishing (#7279) * New Crowdin updates (#7260) * New translations en-US.yaml (Spanish) * New translations en-US.yaml (Spanish) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Russian) * update typescript-eslint monorepo to v4.29.1 (#7283) Co-authored-by: Renovate Bot * Only treat `tinyint(1)` and `tinyint(0)` as booleans (#7287) * added an if catch for tinyint(1) and tinyint(0) * made suggested changes toLowerCase() * update dependency @vue/compiler-sfc to v3.2.0 (#7288) Co-authored-by: Renovate Bot * update dependency vue to v3.2.0 (#7289) Co-authored-by: Renovate Bot * Handle JSON in labels display (#7292) Fixes #7278 * update dependency pinia to v2.0.0-rc.3 (#7055) Co-authored-by: Renovate Bot * update vue monorepo to v3.2.1 (#7293) Co-authored-by: Renovate Bot * Flush caches on server (re)start (#7294) * v9.0.0-rc.89 * Update package-lock * Update release script To workaround breaking change in npm patch :tada: * Update changelog * update dependency pinia to v2.0.0-rc.4 (#7297) Co-authored-by: Renovate Bot * update dependency rollup to v2.56.2 (#7303) Co-authored-by: Renovate Bot * Fix HTTP method for collections.createMany in SDK (#7304) * Fix HTTP method for collections.createMany in SDK * Post collections in data body Co-authored-by: rijkvanzanten * Add perm check for sqlite, upload, extensions dirs (#7310) Co-authored-by: Rijk van Zanten * update dependency eslint-plugin-vue to v7.16.0 (#7300) Co-authored-by: Renovate Bot * Fix uuid resolving in SQLite (#7312) Fixes #7306 * Clear the file payload after file upload (#7315) Fixes #7305 * Improve type checking * Mention TELEMETRY environment variable in docs (#7317) * Mention TELEMETRY environment variable in docs * Add clarification Co-authored-by: rijkvanzanten * Import access from fs-extra instead of fs/promises * Resolve sorting in list-o2m-tree-view on dnd * Fix graphql GET request cache query extraction (#7319) Fixes #7298 * Check for related collection before creation relation (#7323) Fixes #7302 * Fix colors on different types (#7322) Co-authored-by: Rijk van Zanten * group is working on aggregate resolver * Check for non-existing parent pk records (#7331) Fixes #7330 * Schema field types are not translated in the app (#7327) * Fix field type label translations * Use translate-object-values util Co-authored-by: rijkvanzanten * Update release script * Add import ref for TS * Tweak, hopefully fix release flow * getAggregateQuery * clean up payload Co-authored-by: Rijk van Zanten * Treat alias-only fields properly * Add missing translations (#7358) * v9.0.0-rc.90 * Update changelog.md * update dependency nanoid to v3.1.24 (#7365) Co-authored-by: Renovate Bot * update dependency supertest to v6.1.5 (#7360) Co-authored-by: Renovate Bot * update vue monorepo to v3.2.2 (#7355) Co-authored-by: Renovate Bot * filters working avg{id} format with number fields * Fix english string after #7358 (#7371) Fixed wrong string in en-US after #7358 PR * group field working * update dependency nanoid to v3.1.25 (#7375) Co-authored-by: Renovate Bot * update dependency directory-tree to v2.3.0 (#7376) Co-authored-by: Renovate Bot * Export Collection button now shows collection name not table name (#7379) * export collection button to uses name not db name * removed unused var * fixed for review * computed collectionName * Add support for Geometry type, add Map Layout & Interface (#5684) * Added map layout * Cleanup and bug fixes * Removed package-lock * Cleanup and fixes * Small fix * Added back package-lock * Saved camera, autofitting option, bug fixes * Refactor and ui improvements * Improvements * Added seled mode * Removed unused dependency * Changed selection behaviour, cleanup. * update import and dependencies * make custom style into drawer * remove unused imports * use lodash functions * add popups * allow header to become small * reorganize settings * add styling to popup * change default template * add projection option * add basic map interface * finish simple map * add mapbox style * support more mapbox layouts * add api key option * add mapbox backgrounds to layout * warn when no api key is set * fix for latest version * Improved map layout and interface, bug fixes, refactoring. . . * Added postgis geometry format, added marker icon shadow * Made map buttons bigger and their icons thinner. Added transition to header bar. * Bug fixes and error handling in map interface. * Moved box-select control out of the map component. Removed material icons sprite and use addImage for marker support. * Handle MultiGeometry -> Geometry interface error. * Removed hardcoded styles. Added migrations for basemap column. Lots of refactoring. Removed hardcoded styles. Added migrations for basemap column. Lots of refactoring. * Fixed style reloading error. Added translations. * Moved worker code to lib. * Removed worker code. Prevent Mapbox from removing access_token from the URL. * Refactoring. * Change basemap selection to in-map dropdown for layout and interface. * Touchscreen selection support and small fixes. * Small change. * Fixed unused imports. * Added support for PostgreSQL identity column * Renamed migration. Added crs translation. * Only show fields using the map interface in the map layout. * Removed logging. * Reverted Dockerfile change. * Improved crs support. * Fixed translations. * Check for schema identity before updating it. * Fixed popup not updating on feature hover. * Added feature hover styling. Fixed layer customization input. Added out of bounds error handling. * Added geometry type and support for database native geometries. * Fixed linting. * Fixed layout. * Fixed layout. * Actually fixed linting * Full support for native geometries Fixed basemap input Improved feature popup on hover Locked interfaced support * Fixed geometryType option not updating * Bug fixes in interface * Fixed crash when empty basemap settings. Fixed fitBounds option not updating. * Added back storage type option. Improved interface behaviour. * Dropped wkb because of vendor inconsistency with binary data * Updated layout to match new geometry type. Fixed geojson payload transform. * Added missing geometry_format attributes to local types. * Fixed typos & refactoring * Removed dependency on proj4 * Fix error when empty map interface options * Set geometry SRID to 4326 when inserting into the database * Add support for selectMode * Fix error on initial source load * Added geocoder, use GeoJSON for api i/o, removed geometry_format option, refactoring * Added geometry intersects filter. Created geometry helper class. * Fix error when null geometryOptions, added mapbox_key setting. * Moved all geometry parsing/serializing into processGeometries in `payload.ts`. Fixed type errors. * Migrate to Vue 3 * Use wellknown instead of wkx * Fixed basemap selection. * Added available operator for geometry type * Added nintersects filter, fixed map interface for filter input * Added intersects_bbox filter & bug fixes. * Fixed icons rendering * Fixed cursor icon in select mode * Added geometry aggregate function * Fixed geometry processing bug when imported from relational field. * Fixed error with geocoder instanciation * Removed @types/maplibre-gl dependency * Removed fitViewToData options * Merge remote-tracking branch 'upstream/main' into map-layout * Fixed style and geometryType in map interface options * Fixed style change on map interface. * Improved fitViewToData behaviour * Fixed type imports and previous merge conflict * Fixed linting * Added available operators * Fix and merge migrations * Remove outdated p-queue dep * Fix get-schema column extract * Replace pg with postgis for local debugging * Re-add missing import * Add mapbox as a basemap when key exists * Remove unused tz flag * Process delta in payloadservice * Set default map, add limit number styling * Default display template to just PK * Tweak styling of error dialog * Fix method usage in helpers * Move sdo_geo to oracle section * Remove extensions from ts config exclude * Move geo types to shared, remove _Geometry * Remove unused type * Tiny Tweaks * Remove fit to bounds option in favor of on * Validate incoming intersects query * Deepmap filter values * Add GraphQL support * No defaultValue for geometryType * Resolve c * Fix translations Co-authored-by: Nitwel Co-authored-by: Rijk van Zanten * New Crowdin updates (#7359) * New translations en-US.yaml (Estonian) * New translations en-US.yaml (Ukrainian) * New translations en-US.yaml (Norwegian) * New translations en-US.yaml (Polish) * New translations en-US.yaml (Portuguese) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Serbian (Cyrillic)) * New translations en-US.yaml (Swedish) * New translations en-US.yaml (Turkish) * New translations en-US.yaml (Chinese Traditional) * New translations en-US.yaml (Portuguese, Brazilian) * New translations en-US.yaml (Indonesian) * New translations en-US.yaml (Spanish, Chile) * New translations en-US.yaml (Thai) * New translations en-US.yaml (Hindi) * New translations en-US.yaml (Malay) * New translations en-US.yaml (Serbian (Latin)) * New translations en-US.yaml (Dutch) * New translations en-US.yaml (Italian) * New translations en-US.yaml (Afrikaans) * New translations en-US.yaml (Lithuanian) * New translations en-US.yaml (Spanish, Latin America) * New translations en-US.yaml (Slovenian) * New translations en-US.yaml (Vietnamese) * New translations en-US.yaml (Chinese Simplified) * New translations en-US.yaml (Bulgarian) * New translations en-US.yaml (Romanian) * New translations en-US.yaml (French) * New translations en-US.yaml (Spanish) * New translations en-US.yaml (Arabic) * New translations en-US.yaml (Georgian) * New translations en-US.yaml (Catalan) * New translations en-US.yaml (Czech) * New translations en-US.yaml (Danish) * New translations en-US.yaml (German) * New translations en-US.yaml (Greek) * New translations en-US.yaml (Finnish) * New translations en-US.yaml (Hebrew) * New translations en-US.yaml (Hungarian) * New translations en-US.yaml (Japanese) * Update source file en-US.yaml * New translations en-US.yaml (Italian) * New translations en-US.yaml (Slovenian) * New translations en-US.yaml (Estonian) * New translations en-US.yaml (Estonian) * New translations en-US.yaml (Sinhala) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Bulgarian) * Update source file en-US.yaml * New translations en-US.yaml (Estonian) * New translations en-US.yaml (Norwegian) * New translations en-US.yaml (Polish) * New translations en-US.yaml (Portuguese) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Swedish) * New translations en-US.yaml (Turkish) * New translations en-US.yaml (Portuguese, Brazilian) * New translations en-US.yaml (Spanish, Chile) * New translations en-US.yaml (Thai) * New translations en-US.yaml (Serbian (Latin)) * New translations en-US.yaml (Dutch) * New translations en-US.yaml (Lithuanian) * New translations en-US.yaml (Spanish, Latin America) * New translations en-US.yaml (Vietnamese) * New translations en-US.yaml (Chinese Simplified) * New translations en-US.yaml (Bulgarian) * New translations en-US.yaml (French) * New translations en-US.yaml (Spanish) * New translations en-US.yaml (Arabic) * New translations en-US.yaml (German) * New translations en-US.yaml (Finnish) * New translations en-US.yaml (Hungarian) * update dependency directory-tree to v2.3.1 (#7380) Co-authored-by: Renovate Bot * pin dependencies (#7384) Co-authored-by: Renovate Bot * update dependency macos-release to v3 (#7381) * update dependency macos-release to v3 * Update package-lock Co-authored-by: Renovate Bot Co-authored-by: rijkvanzanten * New Crowdin updates (#7386) * Update source file en-US.yaml * New translations en-US.yaml (Estonian) * New translations en-US.yaml (Polish) * New translations en-US.yaml (Portuguese) * New translations en-US.yaml (Russian) * New translations en-US.yaml (Swedish) * New translations en-US.yaml (Turkish) * New translations en-US.yaml (Chinese Traditional) * New translations en-US.yaml (Portuguese, Brazilian) * New translations en-US.yaml (Indonesian) * New translations en-US.yaml (Spanish, Chile) * New translations en-US.yaml (Thai) * New translations en-US.yaml (Serbian (Latin)) * New translations en-US.yaml (Dutch) * New translations en-US.yaml (Italian) * New translations en-US.yaml (Lithuanian) * New translations en-US.yaml (Spanish, Latin America) * New translations en-US.yaml (Slovenian) * New translations en-US.yaml (Vietnamese) * New translations en-US.yaml (Chinese Simplified) * New translations en-US.yaml (Bulgarian) * New translations en-US.yaml (French) * New translations en-US.yaml (Spanish) * New translations en-US.yaml (Arabic) * New translations en-US.yaml (German) * New translations en-US.yaml (Finnish) * New translations en-US.yaml (Hungarian) * Revert "update dependency macos-release to v3 (#7381)" (#7389) This reverts commit ca111a80cb037091aa1203f6f5663dd9c4292245. * update dependency npm to v7.20.6 (#7387) Co-authored-by: Renovate Bot * Fix flat lock number * Small tweaks, fix type bug Co-authored-by: Rijk van Zanten Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renovate Bot Co-authored-by: Nicola Krumschmidt Co-authored-by: Pascal Jufer Co-authored-by: Adrian Dimitrov Co-authored-by: Oreille <33065839+Oreilles@users.noreply.github.com> Co-authored-by: Nitwel * Fix merge quirk * Add support for aliasing fields (#7419) * Don't double split csv values * Still join them on create tho * Add support for `alias` query param * Support aliases in wildcards * Alias Support Within GraphQL (#7410) * graphQL support for aliases * moved aliases to its own function parseAliases * Tweak types Co-authored-by: rijkvanzanten * Fix field resolution on alias usage Fixes #5551 * Add `*_func` resolvers for date/time/datetime/timestamp fields * graphQL Enum for groupby (#7445) Co-authored-by: Rijk van Zanten Co-authored-by: Rijk van Zanten * Docs for Aggregation + Group By + Aliases (#7436) * aggregation docs for graphql * aliases * added REST examples * rest queries * logo max size * Recreate package-lock * Update types/structure * Fix childNode fetching * Fix grouping * Fix time-series * Fix metric panel * Add date func support in filter input graphql * List panel (#8129) * Merge branch 'main' of https://github.com/directus/directus into list-panel * list showing mostly styled. * Add missing options, cleanup * Add editing to list panel type * Tweak sizing Co-authored-by: jaycammarano * Add no-data notice to list panel * Camelcasify show_header * Add cmd+s shortcut * Tweak sizing, fix translation key * Update docs * Add multi-group support to GraphQL * Align syntax of interfaces & panels * Tweak min-size of label panel * Fix linter warnings/errors * Fix totally unrelated issue But I'm here now anyways, so might as well Co-authored-by: Ben Haynes Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Renovate Bot Co-authored-by: Geert Ijewski <51948919+geertijewski@users.noreply.github.com> Co-authored-by: Thijs-Jan <13321277+MoltenCoffee@users.noreply.github.com> Co-authored-by: Nacho GarcĂ­a Co-authored-by: Aiden Foxx Co-authored-by: Aiden Foxx Co-authored-by: Oreille <33065839+Oreilles@users.noreply.github.com> Co-authored-by: Zorin Sergey <36981278+Enhed@users.noreply.github.com> Co-authored-by: Nicola Krumschmidt Co-authored-by: Tommaso Bartolucci Co-authored-by: Jay Cammarano <67079013+jaycammarano@users.noreply.github.com> Co-authored-by: Pascal Jufer Co-authored-by: Adrian Dimitrov Co-authored-by: Nitwel Co-authored-by: jaycammarano --- api/src/services/graphql.ts | 375 +++++++++++++++++++++++++++-- app/package.json | 1 + app/src/main.ts | 9 +- app/src/stores/index.ts | 1 + packages/shared/src/types/index.ts | 1 + 5 files changed, 370 insertions(+), 17 deletions(-) diff --git a/api/src/services/graphql.ts b/api/src/services/graphql.ts index 7e70f475e5c899..b04c74f8de3726 100644 --- a/api/src/services/graphql.ts +++ b/api/src/services/graphql.ts @@ -33,6 +33,7 @@ import { StringValueNode, validate, } from 'graphql'; +import { Filter } from '@directus/shared/types'; import { GraphQLJSON, InputTypeComposer, @@ -43,7 +44,7 @@ import { toInputObjectType, } from 'graphql-compose'; import { Knex } from 'knex'; -import { flatten, get, mapKeys, merge, set, uniq } from 'lodash'; +import { flatten, get, mapKeys, merge, set, uniq, pick, transform, isObject, omit } from 'lodash'; import ms from 'ms'; import { getCache } from '../cache'; import getDatabase from '../database'; @@ -52,7 +53,7 @@ import { ForbiddenException, GraphQLValidationException, InvalidPayloadException import { BaseException } from '@directus/shared/exceptions'; import { listExtensions } from '../extensions'; import { Accountability } from '@directus/shared/types'; -import { AbstractServiceOptions, Action, GraphQLParams, Item, Query, SchemaOverview } from '../types'; +import { AbstractServiceOptions, Action, Aggregate, GraphQLParams, Item, Query, SchemaOverview } from '../types'; import { getGraphQLType } from '../utils/get-graphql-type'; import { reduceSchema } from '../utils/reduce-schema'; import { sanitizeQuery } from '../utils/sanitize-query'; @@ -228,6 +229,22 @@ export class GraphQLService { acc[`${collectionName}_by_id`] = ReadCollectionTypes[collection.collection].getResolver( `${collection.collection}_by_id` ); + + const hasAggregate = Object.values(collection.fields).some((field) => { + const graphqlType = getGraphQLType(field.type); + + if (graphqlType === GraphQLInt || graphqlType === GraphQLFloat) { + return true; + } + + return false; + }); + + if (hasAggregate) { + acc[`${collectionName}_aggregated`] = ReadCollectionTypes[collection.collection].getResolver( + `${collection.collection}_aggregated` + ); + } } return acc; @@ -324,6 +341,50 @@ export class GraphQLService { function getTypes(action: 'read' | 'create' | 'update' | 'delete') { const CollectionTypes: Record = {}; + const DateFunctions = schemaComposer.createObjectTC({ + name: 'date_functions', + fields: { + year: { + type: GraphQLInt, + }, + month: { + type: GraphQLInt, + }, + week: { + type: GraphQLInt, + }, + day: { + type: GraphQLInt, + }, + weekday: { + type: GraphQLInt, + }, + }, + }); + + const TimeFunctions = schemaComposer.createObjectTC({ + name: 'time_functions', + fields: { + hour: { + type: GraphQLInt, + }, + minute: { + type: GraphQLInt, + }, + second: { + type: GraphQLInt, + }, + }, + }); + + const DateTimeFunctions = schemaComposer.createObjectTC({ + name: 'datetime_functions', + fields: { + ...DateFunctions.getFields(), + ...TimeFunctions.getFields(), + }, + }); + for (const collection of Object.values(schema[action].collections)) { if (Object.keys(collection.fields).length === 0) continue; if (SYSTEM_DENY_LIST.includes(collection.collection)) continue; @@ -347,8 +408,41 @@ export class GraphQLService { acc[field.field] = { type, description: field.note, + resolve: (obj: Record, _, __, info) => { + return obj[info?.path?.key ?? field.field]; + }, }; + if (field.type === 'date') { + acc[`${field.field}_func`] = { + type: DateFunctions, + resolve: (obj: Record) => { + const funcFields = Object.keys(DateFunctions.getFields()).map((key) => `${field.field}_${key}`); + return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1)); + }, + }; + } + + if (field.type === 'time') { + acc[`${field.field}_func`] = { + type: TimeFunctions, + resolve: (obj: Record) => { + const funcFields = Object.keys(TimeFunctions.getFields()).map((key) => `${field.field}_${key}`); + return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1)); + }, + }; + } + + if (field.type === 'dateTime' || field.type === 'timestamp') { + acc[`${field.field}_func`] = { + type: DateTimeFunctions, + resolve: (obj: Record) => { + const funcFields = Object.keys(DateTimeFunctions.getFields()).map((key) => `${field.field}_${key}`); + return mapKeys(pick(obj, funcFields), (_value, key) => key.substring(field.field.length + 1)); + }, + }; + } + return acc; }, {} as ObjectTypeComposerFieldConfigMapDefinition), }); @@ -359,6 +453,9 @@ export class GraphQLService { CollectionTypes[relation.collection]?.addFields({ [relation.field]: { type: CollectionTypes[relation.related_collection], + resolve: (obj: Record, _, __, info) => { + return obj[info?.path?.key ?? relation.field]; + }, }, }); @@ -366,6 +463,9 @@ export class GraphQLService { CollectionTypes[relation.related_collection]?.addFields({ [relation.meta.one_field]: { type: [CollectionTypes[relation.collection]], + resolve: (obj: Record, _, __, info) => { + return obj[info?.path?.key ?? relation.meta!.one_field]; + }, }, }); } @@ -397,6 +497,9 @@ export class GraphQLService { return CollectionTypes[collection].getType(); }, }), + resolve: (obj: Record, _, __, info) => { + return obj[info?.path?.key ?? relation.field]; + }, }, }); } @@ -410,8 +513,12 @@ export class GraphQLService { */ function getReadableTypes() { const { CollectionTypes: ReadCollectionTypes } = getTypes('read'); + const ReadableCollectionFilterTypes: Record = {}; + const AggregatedFunctions: Record> = {}; + const AggregatedFilters: Record> = {}; + const StringFilterOperators = schemaComposer.createInputTC({ name: 'string_filter_operators', fields: { @@ -568,6 +675,50 @@ export class GraphQLService { }, }); + const DateFunctionFilterOperators = schemaComposer.createInputTC({ + name: 'date_function_filter_operators', + fields: { + year: { + type: NumberFilterOperators, + }, + month: { + type: NumberFilterOperators, + }, + week: { + type: NumberFilterOperators, + }, + day: { + type: NumberFilterOperators, + }, + weekday: { + type: NumberFilterOperators, + }, + }, + }); + + const TimeFunctionFilterOperators = schemaComposer.createInputTC({ + name: 'time_function_filter_operators', + fields: { + hour: { + type: NumberFilterOperators, + }, + minute: { + type: NumberFilterOperators, + }, + second: { + type: NumberFilterOperators, + }, + }, + }); + + const DateTimeFunctionFilterOperators = schemaComposer.createInputTC({ + name: 'datetime_function_filter_operators', + fields: { + ...DateFunctionFilterOperators.getFields(), + ...TimeFunctionFilterOperators.getFields(), + }, + }); + for (const collection of Object.values(schema.read.collections)) { if (Object.keys(collection.fields).length === 0) continue; if (SYSTEM_DENY_LIST.includes(collection.collection)) continue; @@ -578,6 +729,7 @@ export class GraphQLService { const graphqlType = getGraphQLType(field.type); let filterOperatorType: InputTypeComposer; + switch (graphqlType) { case GraphQLBoolean: filterOperatorType = BooleanFilterOperators; @@ -598,6 +750,24 @@ export class GraphQLService { acc[field.field] = filterOperatorType; + if (field.type === 'date') { + acc[`${field.field}_func`] = { + type: DateFunctionFilterOperators, + }; + } + + if (field.type === 'time') { + acc[`${field.field}_func`] = { + type: TimeFunctionFilterOperators, + }; + } + + if (field.type === 'dateTime' || field.type === 'timestamp') { + acc[`${field.field}_func`] = { + type: DateTimeFunctionFilterOperators, + }; + } + return acc; }, {} as InputTypeComposerFieldConfigMapDefinition), }); @@ -607,6 +777,69 @@ export class GraphQLService { _or: [ReadableCollectionFilterTypes[collection.collection]], }); + AggregatedFilters[collection.collection] = schemaComposer.createObjectTC({ + name: `${collection.collection}_aggregated_fields`, + fields: Object.values(collection.fields).reduce((acc, field) => { + const graphqlType = getGraphQLType(field.type); + + switch (graphqlType) { + case GraphQLInt: + case GraphQLFloat: + acc[field.field] = { + type: GraphQLFloat, + description: field.note, + }; + break; + default: + break; + } + + return acc; + }, {} as ObjectTypeComposerFieldConfigMapDefinition), + }); + + AggregatedFunctions[collection.collection] = schemaComposer.createObjectTC({ + name: `${collection.collection}_aggregated`, + fields: { + group: { + name: 'group', + type: GraphQLJSON, + }, + avg: { + name: 'avg', + type: AggregatedFilters[collection.collection], + }, + sum: { + name: 'sum', + type: AggregatedFilters[collection.collection], + }, + count: { + name: 'count', + type: AggregatedFilters[collection.collection], + }, + countDistinct: { + name: 'countDistinct', + type: AggregatedFilters[collection.collection], + }, + avgDistinct: { + name: 'avgDistinct', + type: AggregatedFilters[collection.collection], + }, + sumDistinct: { + name: 'sumDistinct', + type: AggregatedFilters[collection.collection], + }, + min: { + name: 'min', + type: AggregatedFilters[collection.collection], + }, + max: { + name: 'max', + type: AggregatedFilters[collection.collection], + }, + }, + }); + ReadCollectionTypes[collection.collection].addResolver({ name: collection.collection, args: collection.singleton @@ -639,6 +872,20 @@ export class GraphQLService { }, }); + ReadCollectionTypes[collection.collection].addResolver({ + name: `${collection.collection}_aggregated`, + type: [AggregatedFunctions[collection.collection]], + args: { + groupBy: new GraphQLList(GraphQLString), + }, + resolve: async ({ info, context }: { info: GraphQLResolveInfo; context: Record }) => { + const result = await self.resolveQuery(info); + context.data = result; + + return result; + }, + }); + if (collection.singleton === false) { ReadCollectionTypes[collection.collection].addResolver({ name: `${collection.collection}_by_id`, @@ -871,18 +1118,25 @@ export class GraphQLService { async resolveQuery(info: GraphQLResolveInfo): Promise | null> { let collection = info.fieldName; if (this.scope === 'system') collection = `directus_${collection}`; - const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments); if (!selections) return null; - const args: Record = this.parseArgs(info.fieldNodes[0].arguments || [], info.variableValues); - const query = this.getQuery(args, selections, info.variableValues); - if (collection.endsWith('_by_id') && collection in this.schema.collections === false) { - collection = collection.slice(0, -6); - } + let query: Record; + + const isAggregate = collection.endsWith('_aggregated') && collection in this.schema.collections === false; + if (isAggregate) { + query = this.getAggregateQuery(args, selections); + collection = collection.slice(0, -11); + } else { + query = this.getQuery(args, selections, info.variableValues); + + if (collection.endsWith('_by_id') && collection in this.schema.collections === false) { + collection = collection.slice(0, -6); + } + } if (args.id) { query.filter = { _and: [ @@ -904,13 +1158,18 @@ export class GraphQLService { return result?.[0] || null; } + if (query.group) { + // for every entry in result add a group field based on query.group; + const aggregateKeys = Object.keys(query.aggregate ?? {}); + + result.map((field: Item) => { + field.group = omit(field, aggregateKeys); + }); + } + return result; } - /** - * Generic mutation resolver that converts the incoming GraphQL mutation AST into a Directus query and executes the - * appropriate C-UD operation - */ async resolveMutation( args: Record, info: GraphQLResolveInfo @@ -1068,24 +1327,42 @@ export class GraphQLService { ): Query { const query: Query = sanitizeQuery(rawQuery, this.accountability); + const parseAliases = (selections: readonly SelectionNode[]) => { + const aliases: Record = {}; + + for (const selection of selections) { + if (selection.kind !== 'Field') continue; + + if (selection.alias?.value) { + aliases[selection.alias.value] = selection.name.value; + } + } + + return aliases; + }; + const parseFields = (selections: readonly SelectionNode[], parent?: string): string[] => { const fields: string[] = []; for (let selection of selections) { if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') !== true) continue; + selection = selection as FieldNode | InlineFragmentNode; let current: string; + // Union type (Many-to-Any) if (selection.kind === 'InlineFragment') { - // filter out graphql pointers, like __typename if (selection.typeCondition!.name.value.startsWith('__')) continue; current = `${parent}:${selection.typeCondition!.name.value}`; - } else { + } + // Any other field type + else { // filter out graphql pointers, like __typename if (selection.name.value.startsWith('__')) continue; - current = selection.name.value; + + current = selection.alias?.value ?? selection.name.value; if (parent) { current = `${parent}.${current}`; @@ -1093,7 +1370,20 @@ export class GraphQLService { } if (selection.selectionSet) { - const children = parseFields(selection.selectionSet.selections, current); + let children: string[]; + + if (current.endsWith('_func')) { + children = []; + + const rootField = current.slice(0, -5); + + for (const subSelection of selection.selectionSet.selections) { + if (subSelection.kind !== 'Field') continue; + children.push(`${subSelection.name!.value}(${rootField})`); + } + } else { + children = parseFields(selection.selectionSet.selections, current); + } fields.push(...children); } else { @@ -1121,13 +1411,65 @@ export class GraphQLService { return uniq(fields); }; + const replaceFuncs = (filter?: Filter): undefined | Filter => { + if (!filter) return filter; + + return replaceFuncDeep(filter); + + function replaceFuncDeep(filter: Record) { + return transform(filter, (result: Record, value, key) => { + let currentKey = key; + + if (typeof key === 'string' && key.endsWith('_func')) { + const functionName = Object.keys(value)[0]!; + currentKey = `${functionName}(${currentKey.slice(0, -5)})`; + + result[currentKey] = Object.values(value)[0]!; + } else { + result[currentKey] = isObject(value) ? replaceFuncDeep(value) : value; + } + }); + } + }; + + query.alias = parseAliases(selections); query.fields = parseFields(selections); + query.filter = replaceFuncs(query.filter); validateQuery(query); return query; } + /** + * Resolve the aggregation query based on the requested aggregated fields + */ + getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[]): Query { + const query: Query = sanitizeQuery(rawQuery, this.accountability); + + query.aggregate = {}; + + for (let aggregationGroup of selections) { + if ((aggregationGroup.kind === 'Field') !== true) continue; + + aggregationGroup = aggregationGroup as FieldNode; + + // filter out graphql pointers, like __typename + if (aggregationGroup.name.value.startsWith('__')) continue; + + const aggregateProperty = aggregationGroup.name.value as keyof Aggregate; + + query.aggregate[aggregateProperty] = + aggregationGroup.selectionSet?.selections.map((selectionNode) => { + selectionNode = selectionNode as FieldNode; + return selectionNode.name.value; + }) ?? []; + } + + validateQuery(query); + + return query; + } /** * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly. */ @@ -2074,6 +2416,7 @@ export class GraphQLService { info.fragments ); const query = this.getQuery(args, selections || [], info.variableValues); + return await service.readOne(this.accountability.user, query); }, }, diff --git a/app/package.json b/app/package.json index 98de4ae5fc37df..efc31275fd7a66 100644 --- a/app/package.json +++ b/app/package.json @@ -68,6 +68,7 @@ "@vue/cli-plugin-vuex": "4.5.13", "@vue/cli-service": "4.5.13", "@vue/compiler-sfc": "3.2.12", + "apexcharts": "3.26.3", "axios": "0.21.4", "base-64": "1.0.0", "codemirror": "5.63.0", diff --git a/app/src/main.ts b/app/src/main.ts index 7d84ca2bb229cd..4a20676c5f81af 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -7,6 +7,7 @@ import App from './app.vue'; import { registerComponents } from './components/register'; import { DIRECTUS_LOGO } from './constants'; import { registerDirectives } from './directives/register'; +import { registerPanels } from './panels/register'; import { registerDisplays } from './displays/register'; import { registerInterfaces } from './interfaces/register'; import { i18n } from './lang/'; @@ -42,7 +43,13 @@ async function init() { registerComponents(app); registerViews(app); - await Promise.all([registerInterfaces(app), registerDisplays(app), registerLayouts(app), loadModules()]); + await Promise.all([ + registerInterfaces(app), + registerPanels(app), + registerDisplays(app), + registerLayouts(app), + loadModules(), + ]); app.mount('#app'); diff --git a/app/src/stores/index.ts b/app/src/stores/index.ts index d7fbf37892bc12..4f4c6f4f07e0f9 100644 --- a/app/src/stores/index.ts +++ b/app/src/stores/index.ts @@ -1,6 +1,7 @@ export * from './app'; export * from './collections'; export * from './fields'; +export * from './insights'; export * from './latency'; export * from './notifications'; export * from './permissions'; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 8deedeb50325e3..126e5310f699d0 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -12,6 +12,7 @@ export * from './items'; export * from './layouts'; export * from './misc'; export * from './modules'; +export * from './panels'; export * from './permissions'; export * from './presets'; export * from './relations';