{"id":20253543,"url":"https://github.com/graphile/pg-aggregates","last_synced_at":"2025-04-12T15:42:28.365Z","repository":{"id":37471806,"uuid":"174814511","full_name":"graphile/pg-aggregates","owner":"graphile","description":"Aggregates for PostGraphile connections","archived":false,"fork":false,"pushed_at":"2025-03-25T08:53:23.000Z","size":860,"stargazers_count":85,"open_issues_count":5,"forks_count":17,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-03T17:11:11.716Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/graphile.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"Benjie"}},"created_at":"2019-03-10T11:31:52.000Z","updated_at":"2025-03-27T19:57:39.000Z","dependencies_parsed_at":"2023-10-11T13:35:38.582Z","dependency_job_id":"f047d084-268b-4c98-b455-7b97d0378d91","html_url":"https://github.com/graphile/pg-aggregates","commit_stats":{"total_commits":52,"total_committers":6,"mean_commits":8.666666666666666,"dds":0.6153846153846154,"last_synced_commit":"5f026b2fa367c4d8428cfea252c4bbf45f459f75"},"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpg-aggregates","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpg-aggregates/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpg-aggregates/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/graphile%2Fpg-aggregates/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/graphile","download_url":"https://codeload.github.com/graphile/pg-aggregates/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248590919,"owners_count":21129914,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T10:25:30.982Z","updated_at":"2025-04-12T15:42:28.338Z","avatar_url":"https://github.com/graphile.png","language":"TypeScript","funding_links":["https://github.com/sponsors/Benjie"],"categories":[],"sub_categories":[],"readme":"# @graphile/pg-aggregates\n\nAdds a powerful suite of aggregate functionality to a PostGraphile schema:\ncalculating aggregates, grouped aggregates, applying conditions to grouped\naggregates, ordering by relational aggregates, filtering by the results of\naggregates on related connections, etc.\n\n**IMPORTANT**: aggregates are added to connections, they do _not_ work with\n\"simple collections\".\n\n\u003c!-- SPONSORS_BEGIN --\u003e\n\n## Crowd-funded open-source software\n\nTo help us develop this software sustainably, we ask all individuals and\nbusinesses that use it to help support its ongoing maintenance and development\nvia sponsorship.\n\n### [Click here to find out more about sponsors and sponsorship.](https://www.graphile.org/sponsor/)\n\nAnd please give some love to our featured sponsors 🤩:\n\n\u003ctable\u003e\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://www.the-guild.dev/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/theguild.png\" width=\"90\" height=\"90\" alt=\"The Guild\" /\u003e\u003cbr /\u003eThe Guild\u003c/a\u003e *\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://dovetailapp.com/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/dovetail.png\" width=\"90\" height=\"90\" alt=\"Dovetail\" /\u003e\u003cbr /\u003eDovetail\u003c/a\u003e *\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://stellate.co/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/Stellate.png\" width=\"90\" height=\"90\" alt=\"Stellate\" /\u003e\u003cbr /\u003eStellate\u003c/a\u003e *\u003c/td\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"https://gosteelhead.com/\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/steelhead.svg\" width=\"90\" height=\"90\" alt=\"Steelhead\" /\u003e\u003cbr /\u003eSteelhead\u003c/a\u003e *\u003c/td\u003e\n\u003c/tr\u003e\u003ctr\u003e\n\u003ctd align=\"center\"\u003e\u003ca href=\"\"\u003e\u003cimg src=\"https://graphile.org/images/sponsors/latchbio.jpg\" width=\"90\" height=\"90\" alt=\"LatchBio\" /\u003e\u003cbr /\u003eLatchBio\u003c/a\u003e *\u003c/td\u003e\n\u003c/tr\u003e\u003c/table\u003e\n\n\u003cem\u003e\\* Sponsors the entire Graphile suite\u003c/em\u003e\n\n\u003c!-- SPONSORS_END --\u003e\n\n## Status\n\nThis module is currently \"experimental\" status; we may change any part of it in\na semver minor release.\n\n## Usage\n\nRequires PostGraphile v4.12.0-alpha.0 or higher.\n\nInstall with:\n\n```\nyarn add postgraphile @graphile/pg-aggregates\n```\n\nCLI usage via `--append-plugins`:\n\n```\npostgraphile --append-plugins @graphile/pg-aggregates -c postgres://localhost/my_db ...\n```\n\nLibrary usage via `appendPlugins`:\n\n```ts\nimport PgAggregatesPlugin from \"@graphile/pg-aggregates\";\n// or: const PgAggregatesPlugin = require(\"@graphile/pg-aggregates\").default;\n\nconst middleware = postgraphile(DATABASE_URL, SCHEMAS, {\n  appendPlugins: [PgAggregatesPlugin],\n});\n```\n\nIf you want you could install our [example schema](__tests__/schema.sql) and\nthen issue a GraphQL query such as:\n\n```graphql\nquery GameAggregates {\n  allMatchStats {\n    aggregates {\n      max {\n        points\n        goals\n        saves\n      }\n      min {\n        points\n      }\n    }\n  }\n  allPlayers(orderBy: [MATCH_STATS_BY_PLAYER_ID_SUM_GOALS_ASC]) {\n    nodes {\n      name\n      matchStatsByPlayerId {\n        totalCount\n        aggregates {\n          sum {\n            points\n            goals\n            saves\n          }\n          average {\n            points\n            goals\n            saves\n            teamPosition\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nor:\n\n```graphql\nquery GroupedAggregatesByDerivative {\n  allMatchStats {\n    byDay: groupedAggregates(groupBy: [CREATED_AT_TRUNCATED_TO_DAY]) {\n      keys # The timestamp truncated to the beginning of the day\n      average {\n        points\n      }\n    }\n    byHour: groupedAggregates(groupBy: [CREATED_AT_TRUNCATED_TO_HOUR]) {\n      keys # The timestamp truncated to the beginning of the hour\n      average {\n        points\n      }\n    }\n  }\n}\n```\n\nTo filter by aggregates on related tables, you will also need\n[postgraphile-plugin-connection-filter](https://github.com/graphile-contrib/postgraphile-plugin-connection-filter),\nand you will need to enable `graphileBuildOptions.connectionFilterRelations`\n[as documented here](https://github.com/graphile-contrib/postgraphile-plugin-connection-filter#connectionfilterrelations).\n\n```js\napp.use(\n  postgraphile(DATABASE_URL, SCHEMA_NAME, {\n    graphileBuildOptions: {\n      connectionFilterRelations: true,\n    },\n  })\n);\n```\n\n## Interaction with connection parameters\n\nAggregates respect the conditions/filters of the connection but are unaffected\nby the pagination of the connection (they ignore the\n`first`/`last`/`after`/`before`/`orderBy` parameters). You may retrieve\n(optionally paginated) node data from a connection at the same time as\nretrieving aggregates from it. Aggregates are supported on connections at any\nlevel of the GraphQL query.\n\n## Aggregates\n\nConnection-wide aggregates are available via the `aggregates` field directly on\na GraphQL connection; for example:\n\n```graphql\nquery LoadsOfAggregates {\n  allFilms {\n    aggregates {\n      average {\n        durationInMinutes\n      }\n    }\n  }\n}\n```\n\nWe support the following aggregates out of the box:\n\n- `sum` (applies to number-like fields) - the result of adding all the values\n  together\n- `distinctCount` (applies to all fields) - the count of the number of distinct\n  values\n- `min` (applies to number-like fields) - the smallest value\n- `max` (applies to number-like fields) - the greatest value\n- `average` (applies to number-like fields) - the average (arithmetic mean)\n  value\n- `stddevSample` (applies to number-like fields) - the sample standard deviation\n  of the values\n- `stddevPopulation` (applies to number-like fields) - the population standard\n  deviation of the values\n- `varianceSample` (applies to number-like fields) - the sample variance of the\n  values\n- `variancePopulation` (applies to number-like fields) - the population variance\n  of the values\n\nSee [Defining your own aggregates](#defining-your-own-aggregates) below for\ndetails on how to add your own aggregates.\n\nDifferent aggregates apply to different data types; in general we attempt to add\naggregate entries for each column and\n[computed column function](https://www.graphile.org/postgraphile/computed-columns/)\nthat appears to be compatible with the aggregate.\n\n## Ordering by aggregates\n\nThis plugin automatically adds some additional `orderBy` criteria to your graph\nallowing you to order by aggregates over relations; e.g. you could find the top\n5 players ordered by their average points scored in each match, and grab some\nmore aggregate information about them too:\n\n```graphql\nquery FocussedOrderedAggregate {\n  allPlayers(\n    first: 5\n    orderBy: [MATCH_STATS_BY_PLAYER_ID_AVERAGE_POINTS_DESC]\n  ) {\n    nodes {\n      name\n      matchStatsByPlayerId {\n        totalCount\n        aggregates {\n          sum {\n            goals\n          }\n          average {\n            points\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n## Filtering by aggregates\n\n(You will need `postgraphile-plugin-connection-filter` for this; see above.)\n\n```graphql\nquery PlayersWith9OrMoreSavesInMatchesTheyScoredIn {\n  allPlayers(\n    filter: {\n      matchStatsByPlayerId: {\n        aggregates: {\n          sum: { saves: { greaterThan: \"9\" }, rating: { lessThan: 143 } }\n          filter: { goals: { greaterThan: 0 } }\n        }\n      }\n    }\n  ) {\n    nodes {\n      name\n      matchStatsByPlayerId(filter: { goals: { greaterThan: 0 } }) {\n        aggregates {\n          sum {\n            saves\n            rating\n            goals\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n## Grouped aggregates\n\nWe also support grouping your data via the value of one of your columns or a\nderivative thereof; and calculating aggregates over each of the matching groups.\nOut of the box we support two derivatives:\n\n- `truncated-to-hour` (applies to timestamp-like values) - truncates to the\n  beginning of the (UTC) hour\n- `truncated-to-day` (applies to timestamp-like values) - truncates to the\n  beginning of the (UTC) day\n\nSee\n[Defining your own grouping derivatives](#defining-your-own-grouping-derivatives)\nbelow for details on how to add your own grouping derivatives.\n\nThe aggregates supported over groups are the same as over the connection as a\nwhole (see [Aggregates](#aggregates) above), but in addition you may also\ndetermine the `keys` that were used for the aggregate. There will be one key for\neach of the `groupBy` values; for example in this query:\n\n```graphql\nquery AverageDurationByYearOfRelease {\n  allFilms {\n    groupedAggregates(groupBy: [YEAR_OF_RELEASE]) {\n      keys\n      average {\n        durationInMinutes\n      }\n    }\n  }\n}\n```\n\neach entry in the `groupedAggregates` result will have a `keys` entry that will\nbe a list containing one value which will be the year of release (as a string).\nThe values in the `keys` list are always stringified, this is a known limitation\ndue to interactions with GraphQL.\n\n### Having\n\nIf these grouped aggregates are returning too much data, you can filter the\ngroups down by applying a `having` clause against them; for example you could\nsee the average number of goals on days where the average points score was over\n200:\n\n```graphql\nquery AverageGoalsOnDaysWithAveragePointsOver200 {\n  allMatchStats {\n    byDay: groupedAggregates(\n      groupBy: [CREATED_AT_TRUNCATED_TO_DAY]\n      having: { average: { points: { greaterThan: 200 } } }\n    ) {\n      keys\n      average {\n        goals\n      }\n    }\n  }\n}\n```\n\n## Defining your own aggregates\n\nYou can add your own aggregates by using a plugin to add your own aggregate\nspecs. Aggregate specs aren't too complicated, for example here is a spec that\ncould define the \"min\" aggregate:\n\n```ts\nconst isNumberLike = (pgType) =\u003e pgType.category === \"N\";\n\nconst minSpec = {\n  id: \"min\",\n  humanLabel: \"minimum\",\n  HumanLabel: \"Minimum\",\n  isSuitableType: isNumberLike,\n  sqlAggregateWrap: (sqlFrag) =\u003e sql.fragment`min(${sqlFrag})`,\n};\n```\n\nSee [src/AggregateSpecsPlugin.ts](src/AggregateSpecsPlugin.ts) for more\ndetails/examples.\n\n## Defining your own grouping derivatives\n\nYou may add your own derivatives by adding a group by spec to\n`build.pgAggregateGroupBySpecs` via a plugin. Derivative specs are fairly\nstraightforward, for example here's the spec for \"truncated-to-hour\":\n\n```ts\nconst TIMESTAMP_OID = \"1114\";\nconst TIMESTAMPTZ_OID = \"1184\";\n\nconst truncatedToHourSpec = {\n  // A unique identifier for this spec, will be used to generate its name:\n  id: \"truncated-to-hour\",\n\n  // A filter to determine which column/function return types this derivative\n  // is valid against:\n  isSuitableType: (pgType) =\u003e\n    pgType.id === TIMESTAMP_OID || pgType.id === TIMESTAMPTZ_OID,\n\n  // The actual derivative - given the SQL fragment `sqlFrag` which represents\n  // the column/function call, return a new SQL fragment that represents the\n  // derived value, in this case a truncated timestamp:\n  sqlWrap: (sqlFrag) =\u003e sql.fragment`date_trunc('hour', ${sqlFrag})`,\n};\n```\n\nBuilding that up with a few more different intervals into a full PostGraphile\nplugin, you might write something like:\n\n```ts\n// Constants from PostgreSQL\nconst TIMESTAMP_OID = \"1114\";\nconst TIMESTAMPTZ_OID = \"1184\";\n\n// Determine if a given type is a timestamp/timestamptz\nconst isTimestamp = (pgType) =\u003e\n  pgType.id === TIMESTAMP_OID || pgType.id === TIMESTAMPTZ_OID;\n\n// Build a spec that truncates to the given interval\nconst tsTruncateSpec = (sql, interval) =\u003e ({\n  // `id` has to be unique, derive it from the `interval`:\n  id: `truncated-to-${interval}`,\n\n  // Only apply to timestamp fields:\n  isSuitableType: isTimestamp,\n\n  // Given the column value represented by the SQL fragment `sqlFrag`, wrap it\n  // with a `date_trunc()` call, passing the relevant interval.\n  sqlWrap: (sqlFrag) =\u003e\n    sql.fragment`date_trunc(${sql.literal(interval)}, ${sqlFrag})`,\n});\n\n// This is the PostGraphile plugin; see:\n// https://www.graphile.org/postgraphile/extending/\nconst DateTruncAggregateGroupSpecsPlugin = (builder) =\u003e {\n  builder.hook(\"build\", (build) =\u003e {\n    const { pgSql: sql } = build;\n\n    build.pgAggregateGroupBySpecs = [\n      // Copy all existing specs, except the ones we're replacing\n      ...build.pgAggregateGroupBySpecs.filter(\n        (spec) =\u003e ![\"truncated-to-day\", \"truncated-to-hour\"].includes(spec.id)\n      ),\n\n      // Add our timestamp specs\n      tsTruncateSpec(sql, \"year\"),\n      tsTruncateSpec(sql, \"month\"),\n      tsTruncateSpec(sql, \"week\"),\n      tsTruncateSpec(sql, \"day\"),\n      tsTruncateSpec(sql, \"hour\"),\n      // Other values: microseconds, milliseconds, second, minute, quarter,\n      // decade, century, millennium.\n      // See https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC\n    ];\n\n    return build;\n  });\n};\n\nmodule.exports = DateTruncAggregateGroupSpecsPlugin;\n```\n\nFinally pass this plugin into PostGraphile via `--append-plugins` or\n`appendPlugins: [...]` - see https://www.graphile.org/postgraphile/extending/\n\nSee [src/AggregateSpecsPlugin.ts](src/AggregateSpecsPlugin.ts) for examples and\nmore information.\n\n## Disable aggregates\n\nBy default, aggregates are created for all tables. This significantly increases\nthe size of your GraphQL schema, and could also be a security (DoS) concern as\naggregates can be expensive. We recommend that you use the\n`disableAggregatesByDefault: true` option to disable aggregates by default, and\nthen enable them only for the tables you need:\n\n```ts\nconst middleware = postgraphile(DATABASE_URL, SCHEMAS, {\n  // ...\n  appendPlugins: [\n    // ...\n    PgAggregatesPlugin,\n  ],\n\n  graphileBuildOptions: {\n    // Disable aggregates by default; opt each table in via the `@aggregates` smart tag\n    disableAggregatesByDefault: true,\n  },\n});\n```\n\nEnable aggregates for a specific table:\n\n```json\n\"class\": {\n  \"my_schema.my_table\": {\n    \"tags\": {\n      \"aggregates\": \"on\"\n    }\n  }\n}\n```\n\nor:\n\n```sql\nCOMMENT ON TABLE my_schema.my_table IS E'@aggregates on';\n```\n\nYou also can keep aggregates enabled by default, but disable aggregates for\nspecific tables:\n\n```json\n\"class\": {\n  \"my_schema.my_table\": {\n    \"tags\": {\n      \"aggregates\": \"off\"\n    }\n  }\n}\n```\n\nor:\n\n```sql\nCOMMENT ON TABLE my_schema.my_table IS E'@aggregates off';\n```\n\n## Thanks\n\nThis plugin was started as a proof of concept in 2019 thanks to sponsorship from\nOneGraph, and was made into fully featured released module thanks to sponsorship\nfrom Surge in 2021. It is maintained thanks to the support of\n[Graphile's sponsors](https://graphile.org/sponsor/) - thank you sponsors!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphile%2Fpg-aggregates","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgraphile%2Fpg-aggregates","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgraphile%2Fpg-aggregates/lists"}