@gzl10/nexus-plugin-charts 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @gzl10/nexus-plugin-charts
|
|
2
|
+
|
|
3
|
+
Chart data aggregation and visualization endpoints for Nexus BaaS.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Data aggregation over any collection entity (count, sum, avg, min, max)
|
|
8
|
+
- Grouping by date fields (day, week, month, year)
|
|
9
|
+
- Filter support
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
nexus plugin add @gzl10/nexus-plugin-charts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Funding
|
|
18
|
+
|
|
19
|
+
If you find this plugin useful, consider supporting its development:
|
|
20
|
+
|
|
21
|
+
[](https://buymeacoffee.com/gzl10)
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
1
5
|
// src/charts.service.ts
|
|
2
6
|
var VALID_AGGREGATIONS = /* @__PURE__ */ new Set(["sum", "avg", "count", "min", "max"]);
|
|
3
7
|
var AGGREGATION_SQL = {
|
|
@@ -142,6 +146,7 @@ function createChartRoutes(ctx) {
|
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
// src/index.ts
|
|
149
|
+
var pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
145
150
|
var chartsModule = {
|
|
146
151
|
name: "charts",
|
|
147
152
|
label: { en: "Charts", es: "Gr\xE1ficos" },
|
|
@@ -156,11 +161,11 @@ var chartsModule = {
|
|
|
156
161
|
routePrefix: "/charts"
|
|
157
162
|
};
|
|
158
163
|
var chartsPlugin = {
|
|
159
|
-
name:
|
|
164
|
+
name: pkg.name,
|
|
160
165
|
code: "cht",
|
|
161
166
|
label: { en: "Charts", es: "Gr\xE1ficos" },
|
|
162
167
|
icon: "mdi:chart-line",
|
|
163
|
-
version:
|
|
168
|
+
version: pkg.version,
|
|
164
169
|
description: {
|
|
165
170
|
en: "Chart data aggregation and visualization endpoints",
|
|
166
171
|
es: "Endpoints de agregaci\xF3n y visualizaci\xF3n de datos para gr\xE1ficos"
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/charts.service.ts","../src/charts.routes.ts","../src/index.ts"],"sourcesContent":["import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'\n\n/**\n * Chart Data Service\n *\n * Processes entity data and returns aggregated results for chart visualization.\n * All heavy computation is done server-side to minimize client payload.\n */\n\nexport type AggregationType = 'sum' | 'avg' | 'count' | 'min' | 'max'\n\nconst VALID_AGGREGATIONS = new Set<AggregationType>(['sum', 'avg', 'count', 'min', 'max'])\nconst AGGREGATION_SQL: Record<AggregationType, string> = {\n sum: 'SUM', avg: 'AVG', count: 'COUNT', min: 'MIN', max: 'MAX'\n}\n\n/** Safe SQL identifier: letters, digits, underscores. No dots, spaces, or special chars. */\nconst SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/\n\nexport interface ChartDataRequest {\n /** Entity/table name to query */\n entity: string\n /** FilterOperators to apply */\n filters?: Record<string, unknown>\n /** Field to group by */\n groupBy?: string\n /** Aggregation function */\n aggregation?: AggregationType\n /** Field for X-axis */\n xAxis: string\n /** Field for Y-axis (to aggregate) */\n yAxis: string\n /** Maximum rows to return */\n limit?: number\n /** Sort order */\n sort?: 'asc' | 'desc'\n}\n\nexport interface ChartDataPoint {\n x: string | number\n y: number\n label?: string\n group?: string\n}\n\nexport interface ChartDataResponse {\n data: ChartDataPoint[]\n total: number\n aggregation?: AggregationType\n groupBy?: string\n}\n\n/**\n * Validates a field name is a safe SQL identifier to prevent injection.\n * Rejects anything that isn't a simple column name (letters, digits, underscores).\n */\nfunction assertSafeIdentifier(name: string, label: string): void {\n if (!SAFE_IDENTIFIER_RE.test(name)) {\n throw new Error(`Invalid ${label}: \"${name}\" is not a valid column name`)\n }\n}\n\nexport function createChartService(ctx: ModuleContext) {\n const { db } = ctx\n\n /**\n * Get aggregated chart data from an entity\n */\n async function getChartData(request: ChartDataRequest): Promise<ChartDataResponse> {\n const {\n entity,\n filters = {},\n groupBy,\n aggregation = 'sum',\n xAxis,\n yAxis,\n limit = 100,\n sort = 'asc'\n } = request\n\n // Validate aggregation type\n if (!VALID_AGGREGATIONS.has(aggregation)) {\n throw new Error(`Invalid aggregation: \"${aggregation}\"`)\n }\n\n // Validate all field names are safe SQL identifiers\n assertSafeIdentifier(xAxis, 'xAxis')\n assertSafeIdentifier(yAxis, 'yAxis')\n if (groupBy) assertSafeIdentifier(groupBy, 'groupBy')\n\n const aggFn = AGGREGATION_SQL[aggregation]\n const knex = db.knex\n let query = knex(entity)\n\n // Apply filters using the existing filter helper\n if (Object.keys(filters).length > 0) {\n const mod = await import('@gzl10/nexus-backend') as unknown as { applyFilters: (qb: Knex.QueryBuilder, filters: Record<string, unknown>) => Knex.QueryBuilder }\n const { applyFilters } = mod\n query = applyFilters(query, filters)\n }\n\n // Field names are validated by assertSafeIdentifier above.\n // Use ?? (identifier binding) in knex.raw() for aggregation expressions.\n const clampedLimit = Math.min(Math.max(limit, 1), 5000)\n\n if (groupBy) {\n query = query\n .select(xAxis, groupBy)\n .select(knex.raw(`${aggFn}(??) as ??`, [yAxis, 'value']))\n .groupBy(xAxis, groupBy)\n .orderBy(xAxis, sort)\n .limit(clampedLimit)\n } else {\n query = query\n .select(xAxis)\n .select(knex.raw(`${aggFn}(??) as ??`, [yAxis, 'value']))\n .groupBy(xAxis)\n .orderBy(xAxis, sort)\n .limit(clampedLimit)\n }\n\n const rows = await query\n\n const data: ChartDataPoint[] = rows.map((row: Record<string, unknown>) => ({\n x: row[xAxis] as string | number,\n y: Number(row['value']) || 0,\n label: String(row[xAxis]),\n group: groupBy ? String(row[groupBy]) : undefined\n }))\n\n return {\n data,\n total: data.length,\n aggregation,\n groupBy\n }\n }\n\n /**\n * Get raw data without aggregation (for scatter plots, etc.)\n */\n async function getRawData(\n entity: string,\n fields: string[],\n filters: Record<string, unknown> = {},\n limit = 1000\n ): Promise<Record<string, unknown>[]> {\n // Validate all field names\n for (const field of fields) {\n assertSafeIdentifier(field, 'field')\n }\n\n const knex = db.knex\n let query = knex(entity).select(fields).limit(Math.min(Math.max(limit, 1), 5000))\n\n if (Object.keys(filters).length > 0) {\n const mod = await import('@gzl10/nexus-backend') as unknown as { applyFilters: (qb: Knex.QueryBuilder, filters: Record<string, unknown>) => Knex.QueryBuilder }\n const { applyFilters } = mod\n query = applyFilters(query, filters)\n }\n\n return await query\n }\n\n return {\n getChartData,\n getRawData\n }\n}\n","import type { ModuleContext, AuthRequest } from '@gzl10/nexus-sdk'\nimport { createChartService, type ChartDataRequest } from './charts.service.js'\n\n/**\n * Chart Routes\n *\n * REST endpoints for fetching aggregated chart data.\n */\nexport function createChartRoutes(ctx: ModuleContext) {\n const router = ctx.core.createRouter()\n const chartService = createChartService(ctx)\n const auth = ctx.core.middleware['auth']\n\n if (!auth) {\n throw new Error('Auth middleware is required for chart routes')\n }\n\n /**\n * POST /charts/data\n *\n * Get aggregated chart data from an entity.\n * Body: ChartDataRequest\n */\n router.post('/data', auth, async (req, res, next) => {\n try {\n const request = req.body as ChartDataRequest\n\n // Validate required fields\n if (!request.entity || !request.xAxis || !request.yAxis) {\n throw new ctx.core.errors.ValidationError(\n 'Missing required fields: entity, xAxis, yAxis'\n )\n }\n\n // Validate entity is registered and check CASL access\n const subject = ctx.engine.getSubjectForTable(request.entity)\n if (!subject) {\n throw new ctx.core.errors.ValidationError(`Unknown entity: ${request.entity}`)\n }\n\n const ability = (req as AuthRequest).ability\n if (!ability.can('read', subject)) {\n throw new ctx.core.errors.ForbiddenError(`Access denied to ${subject}`)\n }\n\n const result = await chartService.getChartData(request)\n res.json(result)\n } catch (error) {\n next(error)\n }\n })\n\n /**\n * GET /charts/raw/:entity\n *\n * Get raw entity data for custom chart processing.\n * Query params: fields (comma-separated), limit\n */\n router.get('/raw/:entity', auth, async (req, res, next) => {\n try {\n const entity = req.params['entity']\n const { fields, limit } = req.query\n\n if (!entity) {\n throw new ctx.core.errors.ValidationError('Missing entity parameter')\n }\n\n if (!fields || typeof fields !== 'string') {\n throw new ctx.core.errors.ValidationError(\n 'Missing required query param: fields (comma-separated)'\n )\n }\n\n // Validate entity is registered and check CASL access\n const subject = ctx.engine.getSubjectForTable(entity)\n if (!subject) {\n throw new ctx.core.errors.ValidationError(`Unknown entity: ${entity}`)\n }\n\n const ability = (req as AuthRequest).ability\n if (!ability.can('read', subject)) {\n throw new ctx.core.errors.ForbiddenError(`Access denied to ${subject}`)\n }\n\n const fieldList = fields.split(',').map(f => f.trim())\n const limitNum = typeof limit === 'string' ? Number(limit) : 1000\n const maxRows = Math.min(limitNum || 1000, 5000)\n\n const result = await chartService.getRawData(entity, fieldList, {}, maxRows)\n res.json({ data: result, total: result.length })\n } catch (error) {\n next(error)\n }\n })\n\n return router\n}\n","import type { PluginManifest, ModuleManifest } from '@gzl10/nexus-sdk'\nimport { createChartRoutes } from './charts.routes.js'\n\nexport { createChartService, type ChartDataRequest, type ChartDataResponse, type ChartDataPoint, type AggregationType } from './charts.service.js'\n\nconst chartsModule: ModuleManifest = {\n name: 'charts',\n label: { en: 'Charts', es: 'Gráficos' },\n icon: 'mdi:chart-line',\n description: {\n en: 'Chart data aggregation and visualization endpoints',\n es: 'Endpoints de agregación y visualización de datos para gráficos'\n },\n category: 'content',\n definitions: [],\n routes: createChartRoutes,\n routePrefix: '/charts'\n}\n\nexport { chartsModule }\n\nexport const chartsPlugin: PluginManifest = {\n name: '@gzl10/nexus-plugin-charts',\n code: 'cht',\n label: { en: 'Charts', es: 'Gráficos' },\n icon: 'mdi:chart-line',\n version: '0.13.1',\n description: {\n en: 'Chart data aggregation and visualization endpoints',\n es: 'Endpoints de agregación y visualización de datos para gráficos'\n },\n category: 'content',\n modules: [chartsModule]\n}\n\nexport default chartsPlugin\n"],"mappings":";AAWA,IAAM,qBAAqB,oBAAI,IAAqB,CAAC,OAAO,OAAO,SAAS,OAAO,KAAK,CAAC;AACzF,IAAM,kBAAmD;AAAA,EACvD,KAAK;AAAA,EAAO,KAAK;AAAA,EAAO,OAAO;AAAA,EAAS,KAAK;AAAA,EAAO,KAAK;AAC3D;AAGA,IAAM,qBAAqB;AAuC3B,SAAS,qBAAqB,MAAc,OAAqB;AAC/D,MAAI,CAAC,mBAAmB,KAAK,IAAI,GAAG;AAClC,UAAM,IAAI,MAAM,WAAW,KAAK,MAAM,IAAI,8BAA8B;AAAA,EAC1E;AACF;AAEO,SAAS,mBAAmB,KAAoB;AACrD,QAAM,EAAE,GAAG,IAAI;AAKf,iBAAe,aAAa,SAAuD;AACjF,UAAM;AAAA,MACJ;AAAA,MACA,UAAU,CAAC;AAAA,MACX;AAAA,MACA,cAAc;AAAA,MACd;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,OAAO;AAAA,IACT,IAAI;AAGJ,QAAI,CAAC,mBAAmB,IAAI,WAAW,GAAG;AACxC,YAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG;AAAA,IACzD;AAGA,yBAAqB,OAAO,OAAO;AACnC,yBAAqB,OAAO,OAAO;AACnC,QAAI,QAAS,sBAAqB,SAAS,SAAS;AAEpD,UAAM,QAAQ,gBAAgB,WAAW;AACzC,UAAM,OAAO,GAAG;AAChB,QAAI,QAAQ,KAAK,MAAM;AAGvB,QAAI,OAAO,KAAK,OAAO,EAAE,SAAS,GAAG;AACnC,YAAM,MAAM,MAAM,OAAO,sBAAsB;AAC/C,YAAM,EAAE,aAAa,IAAI;AACzB,cAAQ,aAAa,OAAO,OAAO;AAAA,IACrC;AAIA,UAAM,eAAe,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,GAAI;AAEtD,QAAI,SAAS;AACX,cAAQ,MACL,OAAO,OAAO,OAAO,EACrB,OAAO,KAAK,IAAI,GAAG,KAAK,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,EACvD,QAAQ,OAAO,OAAO,EACtB,QAAQ,OAAO,IAAI,EACnB,MAAM,YAAY;AAAA,IACvB,OAAO;AACL,cAAQ,MACL,OAAO,KAAK,EACZ,OAAO,KAAK,IAAI,GAAG,KAAK,cAAc,CAAC,OAAO,OAAO,CAAC,CAAC,EACvD,QAAQ,KAAK,EACb,QAAQ,OAAO,IAAI,EACnB,MAAM,YAAY;AAAA,IACvB;AAEA,UAAM,OAAO,MAAM;AAEnB,UAAM,OAAyB,KAAK,IAAI,CAAC,SAAkC;AAAA,MACzE,GAAG,IAAI,KAAK;AAAA,MACZ,GAAG,OAAO,IAAI,OAAO,CAAC,KAAK;AAAA,MAC3B,OAAO,OAAO,IAAI,KAAK,CAAC;AAAA,MACxB,OAAO,UAAU,OAAO,IAAI,OAAO,CAAC,IAAI;AAAA,IAC1C,EAAE;AAEF,WAAO;AAAA,MACL;AAAA,MACA,OAAO,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAKA,iBAAe,WACb,QACA,QACA,UAAmC,CAAC,GACpC,QAAQ,KAC4B;AAEpC,eAAW,SAAS,QAAQ;AAC1B,2BAAqB,OAAO,OAAO;AAAA,IACrC;AAEA,UAAM,OAAO,GAAG;AAChB,QAAI,QAAQ,KAAK,MAAM,EAAE,OAAO,MAAM,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,OAAO,CAAC,GAAG,GAAI,CAAC;AAEhF,QAAI,OAAO,KAAK,OAAO,EAAE,SAAS,GAAG;AACnC,YAAM,MAAM,MAAM,OAAO,sBAAsB;AAC/C,YAAM,EAAE,aAAa,IAAI;AACzB,cAAQ,aAAa,OAAO,OAAO;AAAA,IACrC;AAEA,WAAO,MAAM;AAAA,EACf;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;;;AChKO,SAAS,kBAAkB,KAAoB;AACpD,QAAM,SAAS,IAAI,KAAK,aAAa;AACrC,QAAM,eAAe,mBAAmB,GAAG;AAC3C,QAAM,OAAO,IAAI,KAAK,WAAW,MAAM;AAEvC,MAAI,CAAC,MAAM;AACT,UAAM,IAAI,MAAM,8CAA8C;AAAA,EAChE;AAQA,SAAO,KAAK,SAAS,MAAM,OAAO,KAAK,KAAK,SAAS;AACnD,QAAI;AACF,YAAM,UAAU,IAAI;AAGpB,UAAI,CAAC,QAAQ,UAAU,CAAC,QAAQ,SAAS,CAAC,QAAQ,OAAO;AACvD,cAAM,IAAI,IAAI,KAAK,OAAO;AAAA,UACxB;AAAA,QACF;AAAA,MACF;AAGA,YAAM,UAAU,IAAI,OAAO,mBAAmB,QAAQ,MAAM;AAC5D,UAAI,CAAC,SAAS;AACZ,cAAM,IAAI,IAAI,KAAK,OAAO,gBAAgB,mBAAmB,QAAQ,MAAM,EAAE;AAAA,MAC/E;AAEA,YAAM,UAAW,IAAoB;AACrC,UAAI,CAAC,QAAQ,IAAI,QAAQ,OAAO,GAAG;AACjC,cAAM,IAAI,IAAI,KAAK,OAAO,eAAe,oBAAoB,OAAO,EAAE;AAAA,MACxE;AAEA,YAAM,SAAS,MAAM,aAAa,aAAa,OAAO;AACtD,UAAI,KAAK,MAAM;AAAA,IACjB,SAAS,OAAO;AACd,WAAK,KAAK;AAAA,IACZ;AAAA,EACF,CAAC;AAQD,SAAO,IAAI,gBAAgB,MAAM,OAAO,KAAK,KAAK,SAAS;AACzD,QAAI;AACF,YAAM,SAAS,IAAI,OAAO,QAAQ;AAClC,YAAM,EAAE,QAAQ,MAAM,IAAI,IAAI;AAE9B,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,IAAI,KAAK,OAAO,gBAAgB,0BAA0B;AAAA,MACtE;AAEA,UAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC,cAAM,IAAI,IAAI,KAAK,OAAO;AAAA,UACxB;AAAA,QACF;AAAA,MACF;AAGA,YAAM,UAAU,IAAI,OAAO,mBAAmB,MAAM;AACpD,UAAI,CAAC,SAAS;AACZ,cAAM,IAAI,IAAI,KAAK,OAAO,gBAAgB,mBAAmB,MAAM,EAAE;AAAA,MACvE;AAEA,YAAM,UAAW,IAAoB;AACrC,UAAI,CAAC,QAAQ,IAAI,QAAQ,OAAO,GAAG;AACjC,cAAM,IAAI,IAAI,KAAK,OAAO,eAAe,oBAAoB,OAAO,EAAE;AAAA,MACxE;AAEA,YAAM,YAAY,OAAO,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC;AACrD,YAAM,WAAW,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI;AAC7D,YAAM,UAAU,KAAK,IAAI,YAAY,KAAM,GAAI;AAE/C,YAAM,SAAS,MAAM,aAAa,WAAW,QAAQ,WAAW,CAAC,GAAG,OAAO;AAC3E,UAAI,KAAK,EAAE,MAAM,QAAQ,OAAO,OAAO,OAAO,CAAC;AAAA,IACjD,SAAS,OAAO;AACd,WAAK,KAAK;AAAA,IACZ;AAAA,EACF,CAAC;AAED,SAAO;AACT;;;AC3FA,IAAM,eAA+B;AAAA,EACnC,MAAM;AAAA,EACN,OAAO,EAAE,IAAI,UAAU,IAAI,cAAW;AAAA,EACtC,MAAM;AAAA,EACN,aAAa;AAAA,IACX,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,UAAU;AAAA,EACV,aAAa,CAAC;AAAA,EACd,QAAQ;AAAA,EACR,aAAa;AACf;AAIO,IAAM,eAA+B;AAAA,EAC1C,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO,EAAE,IAAI,UAAU,IAAI,cAAW;AAAA,EACtC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,aAAa;AAAA,IACX,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,UAAU;AAAA,EACV,SAAS,CAAC,YAAY;AACxB;AAEA,IAAO,gBAAQ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/charts.service.ts","../src/charts.routes.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { PluginManifest, ModuleManifest } from '@gzl10/nexus-sdk'\nimport { createChartRoutes } from './charts.routes.js'\n\nconst pkg = JSON.parse(readFileSync(join(import.meta.dirname, '..', 'package.json'), 'utf-8')) as { name: string; version: string }\n\nexport { createChartService, type ChartDataRequest, type ChartDataResponse, type ChartDataPoint, type AggregationType } from './charts.service.js'\n\nconst chartsModule: ModuleManifest = {\n name: 'charts',\n label: { en: 'Charts', es: 'Gráficos' },\n icon: 'mdi:chart-line',\n description: {\n en: 'Chart data aggregation and visualization endpoints',\n es: 'Endpoints de agregación y visualización de datos para gráficos'\n },\n category: 'content',\n definitions: [],\n routes: createChartRoutes,\n routePrefix: '/charts'\n}\n\nexport { chartsModule }\n\nexport const chartsPlugin: PluginManifest = {\n name: pkg.name,\n code: 'cht',\n label: { en: 'Charts', es: 'Gráficos' },\n icon: 'mdi:chart-line',\n version: pkg.version,\n description: {\n en: 'Chart data aggregation and visualization endpoints',\n es: 'Endpoints de agregación y visualización de datos para gráficos'\n },\n category: 'content',\n modules: [chartsModule]\n}\n\nexport default chartsPlugin\n","import type { ModuleContext, Knex } from '@gzl10/nexus-sdk'\n\n/**\n * Chart Data Service\n *\n * Processes entity data and returns aggregated results for chart visualization.\n * All heavy computation is done server-side to minimize client payload.\n */\n\nexport type AggregationType = 'sum' | 'avg' | 'count' | 'min' | 'max'\n\nconst VALID_AGGREGATIONS = new Set<AggregationType>(['sum', 'avg', 'count', 'min', 'max'])\nconst AGGREGATION_SQL: Record<AggregationType, string> = {\n sum: 'SUM', avg: 'AVG', count: 'COUNT', min: 'MIN', max: 'MAX'\n}\n\n/** Safe SQL identifier: letters, digits, underscores. No dots, spaces, or special chars. */\nconst SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/\n\nexport interface ChartDataRequest {\n /** Entity/table name to query */\n entity: string\n /** FilterOperators to apply */\n filters?: Record<string, unknown>\n /** Field to group by */\n groupBy?: string\n /** Aggregation function */\n aggregation?: AggregationType\n /** Field for X-axis */\n xAxis: string\n /** Field for Y-axis (to aggregate) */\n yAxis: string\n /** Maximum rows to return */\n limit?: number\n /** Sort order */\n sort?: 'asc' | 'desc'\n}\n\nexport interface ChartDataPoint {\n x: string | number\n y: number\n label?: string\n group?: string\n}\n\nexport interface ChartDataResponse {\n data: ChartDataPoint[]\n total: number\n aggregation?: AggregationType\n groupBy?: string\n}\n\n/**\n * Validates a field name is a safe SQL identifier to prevent injection.\n * Rejects anything that isn't a simple column name (letters, digits, underscores).\n */\nfunction assertSafeIdentifier(name: string, label: string): void {\n if (!SAFE_IDENTIFIER_RE.test(name)) {\n throw new Error(`Invalid ${label}: \"${name}\" is not a valid column name`)\n }\n}\n\nexport function createChartService(ctx: ModuleContext) {\n const { db } = ctx\n\n /**\n * Get aggregated chart data from an entity\n */\n async function getChartData(request: ChartDataRequest): Promise<ChartDataResponse> {\n const {\n entity,\n filters = {},\n groupBy,\n aggregation = 'sum',\n xAxis,\n yAxis,\n limit = 100,\n sort = 'asc'\n } = request\n\n // Validate aggregation type\n if (!VALID_AGGREGATIONS.has(aggregation)) {\n throw new Error(`Invalid aggregation: \"${aggregation}\"`)\n }\n\n // Validate all field names are safe SQL identifiers\n assertSafeIdentifier(xAxis, 'xAxis')\n assertSafeIdentifier(yAxis, 'yAxis')\n if (groupBy) assertSafeIdentifier(groupBy, 'groupBy')\n\n const aggFn = AGGREGATION_SQL[aggregation]\n const knex = db.knex\n let query = knex(entity)\n\n // Apply filters using the existing filter helper\n if (Object.keys(filters).length > 0) {\n const mod = await import('@gzl10/nexus-backend') as unknown as { applyFilters: (qb: Knex.QueryBuilder, filters: Record<string, unknown>) => Knex.QueryBuilder }\n const { applyFilters } = mod\n query = applyFilters(query, filters)\n }\n\n // Field names are validated by assertSafeIdentifier above.\n // Use ?? (identifier binding) in knex.raw() for aggregation expressions.\n const clampedLimit = Math.min(Math.max(limit, 1), 5000)\n\n if (groupBy) {\n query = query\n .select(xAxis, groupBy)\n .select(knex.raw(`${aggFn}(??) as ??`, [yAxis, 'value']))\n .groupBy(xAxis, groupBy)\n .orderBy(xAxis, sort)\n .limit(clampedLimit)\n } else {\n query = query\n .select(xAxis)\n .select(knex.raw(`${aggFn}(??) as ??`, [yAxis, 'value']))\n .groupBy(xAxis)\n .orderBy(xAxis, sort)\n .limit(clampedLimit)\n }\n\n const rows = await query\n\n const data: ChartDataPoint[] = rows.map((row: Record<string, unknown>) => ({\n x: row[xAxis] as string | number,\n y: Number(row['value']) || 0,\n label: String(row[xAxis]),\n group: groupBy ? String(row[groupBy]) : undefined\n }))\n\n return {\n data,\n total: data.length,\n aggregation,\n groupBy\n }\n }\n\n /**\n * Get raw data without aggregation (for scatter plots, etc.)\n */\n async function getRawData(\n entity: string,\n fields: string[],\n filters: Record<string, unknown> = {},\n limit = 1000\n ): Promise<Record<string, unknown>[]> {\n // Validate all field names\n for (const field of fields) {\n assertSafeIdentifier(field, 'field')\n }\n\n const knex = db.knex\n let query = knex(entity).select(fields).limit(Math.min(Math.max(limit, 1), 5000))\n\n if (Object.keys(filters).length > 0) {\n const mod = await import('@gzl10/nexus-backend') as unknown as { applyFilters: (qb: Knex.QueryBuilder, filters: Record<string, unknown>) => Knex.QueryBuilder }\n const { applyFilters } = mod\n query = applyFilters(query, filters)\n }\n\n return await query\n }\n\n return {\n getChartData,\n getRawData\n }\n}\n","import type { ModuleContext, AuthRequest } from '@gzl10/nexus-sdk'\nimport { createChartService, type ChartDataRequest } from './charts.service.js'\n\n/**\n * Chart Routes\n *\n * REST endpoints for fetching aggregated chart data.\n */\nexport function createChartRoutes(ctx: ModuleContext) {\n const router = ctx.core.createRouter()\n const chartService = createChartService(ctx)\n const auth = ctx.core.middleware['auth']\n\n if (!auth) {\n throw new Error('Auth middleware is required for chart routes')\n }\n\n /**\n * POST /charts/data\n *\n * Get aggregated chart data from an entity.\n * Body: ChartDataRequest\n */\n router.post('/data', auth, async (req, res, next) => {\n try {\n const request = req.body as ChartDataRequest\n\n // Validate required fields\n if (!request.entity || !request.xAxis || !request.yAxis) {\n throw new ctx.core.errors.ValidationError(\n 'Missing required fields: entity, xAxis, yAxis'\n )\n }\n\n // Validate entity is registered and check CASL access\n const subject = ctx.engine.getSubjectForTable(request.entity)\n if (!subject) {\n throw new ctx.core.errors.ValidationError(`Unknown entity: ${request.entity}`)\n }\n\n const ability = (req as AuthRequest).ability\n if (!ability.can('read', subject)) {\n throw new ctx.core.errors.ForbiddenError(`Access denied to ${subject}`)\n }\n\n const result = await chartService.getChartData(request)\n res.json(result)\n } catch (error) {\n next(error)\n }\n })\n\n /**\n * GET /charts/raw/:entity\n *\n * Get raw entity data for custom chart processing.\n * Query params: fields (comma-separated), limit\n */\n router.get('/raw/:entity', auth, async (req, res, next) => {\n try {\n const entity = req.params['entity']\n const { fields, limit } = req.query\n\n if (!entity) {\n throw new ctx.core.errors.ValidationError('Missing entity parameter')\n }\n\n if (!fields || typeof fields !== 'string') {\n throw new ctx.core.errors.ValidationError(\n 'Missing required query param: fields (comma-separated)'\n )\n }\n\n // Validate entity is registered and check CASL access\n const subject = ctx.engine.getSubjectForTable(entity)\n if (!subject) {\n throw new ctx.core.errors.ValidationError(`Unknown entity: ${entity}`)\n }\n\n const ability = (req as AuthRequest).ability\n if (!ability.can('read', subject)) {\n throw new ctx.core.errors.ForbiddenError(`Access denied to ${subject}`)\n }\n\n const fieldList = fields.split(',').map(f => f.trim())\n const limitNum = typeof limit === 'string' ? Number(limit) : 1000\n const maxRows = Math.min(limitNum || 1000, 5000)\n\n const result = await chartService.getRawData(entity, fieldList, {}, maxRows)\n res.json({ data: result, total: result.length })\n } catch (error) {\n next(error)\n }\n })\n\n return router\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gzl10/nexus-plugin-charts",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Chart data aggregation and visualization plugin for Nexus BaaS",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"@gzl10/nexus-backend": ">=0.13.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@gzl10/nexus-sdk": "0.16.
|
|
28
|
-
"@gzl10/nexus-client": "0.16.
|
|
27
|
+
"@gzl10/nexus-sdk": "0.16.2",
|
|
28
|
+
"@gzl10/nexus-client": "0.16.2"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsup",
|