@panoboard/core 1.11.6 → 1.12.0
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/dist/components/PanelWrapper.js +50 -5
- package/dist/components/PanelWrapper.js.map +1 -1
- package/dist/components/panels/TablePanel.d.ts +7 -1
- package/dist/components/panels/TablePanel.d.ts.map +1 -1
- package/dist/components/panels/TablePanel.js +38 -23
- package/dist/components/panels/TablePanel.js.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +1 -1
- package/dist/schemas/dashboard.d.ts +1 -0
- package/dist/schemas/dashboard.d.ts.map +1 -1
- package/dist/schemas/datasource.d.ts +118 -0
- package/dist/schemas/datasource.d.ts.map +1 -1
- package/dist/schemas/datasource.js +46 -0
- package/dist/schemas/datasource.js.map +1 -1
- package/dist/transforms/computed.d.ts +1 -1
- package/dist/transforms/computed.d.ts.map +1 -1
- package/dist/transforms/engine.d.ts +1 -1
- package/dist/transforms/engine.d.ts.map +1 -1
- package/dist/transforms/filter.d.ts +1 -1
- package/dist/transforms/filter.d.ts.map +1 -1
- package/dist/transforms/limit.d.ts +1 -1
- package/dist/transforms/limit.d.ts.map +1 -1
- package/dist/transforms/percentage.d.ts +1 -1
- package/dist/transforms/percentage.d.ts.map +1 -1
- package/dist/transforms/rename.d.ts +1 -1
- package/dist/transforms/rename.d.ts.map +1 -1
- package/dist/transforms/sort.d.ts +1 -1
- package/dist/transforms/sort.d.ts.map +1 -1
- package/dist/{connectors/types.d.ts → types/connector-types.d.ts} +8 -2
- package/dist/types/connector-types.d.ts.map +1 -0
- package/dist/{query/strategy/types.js → types/connector-types.js} +1 -1
- package/dist/types/connector-types.js.map +1 -0
- package/package.json +2 -39
- package/dist/annotations/executor.d.ts +0 -32
- package/dist/annotations/executor.d.ts.map +0 -1
- package/dist/annotations/executor.js +0 -87
- package/dist/annotations/executor.js.map +0 -1
- package/dist/annotations/index.d.ts +0 -2
- package/dist/annotations/index.d.ts.map +0 -1
- package/dist/annotations/index.js +0 -15
- package/dist/annotations/index.js.map +0 -1
- package/dist/cache/cache.d.ts +0 -11
- package/dist/cache/cache.d.ts.map +0 -1
- package/dist/cache/cache.js +0 -42
- package/dist/cache/cache.js.map +0 -1
- package/dist/cache/client.d.ts +0 -9
- package/dist/cache/client.d.ts.map +0 -1
- package/dist/cache/client.js +0 -63
- package/dist/cache/client.js.map +0 -1
- package/dist/cache/index.d.ts +0 -4
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/index.js +0 -17
- package/dist/cache/index.js.map +0 -1
- package/dist/cache/keys.d.ts +0 -2
- package/dist/cache/keys.d.ts.map +0 -1
- package/dist/cache/keys.js +0 -26
- package/dist/cache/keys.js.map +0 -1
- package/dist/connectors/bigquery.d.ts +0 -26
- package/dist/connectors/bigquery.d.ts.map +0 -1
- package/dist/connectors/bigquery.js +0 -118
- package/dist/connectors/bigquery.js.map +0 -1
- package/dist/connectors/index.d.ts +0 -17
- package/dist/connectors/index.d.ts.map +0 -1
- package/dist/connectors/index.js +0 -23
- package/dist/connectors/index.js.map +0 -1
- package/dist/connectors/params.d.ts +0 -15
- package/dist/connectors/params.d.ts.map +0 -1
- package/dist/connectors/params.js +0 -46
- package/dist/connectors/params.js.map +0 -1
- package/dist/connectors/postgres.d.ts +0 -29
- package/dist/connectors/postgres.d.ts.map +0 -1
- package/dist/connectors/postgres.js +0 -133
- package/dist/connectors/postgres.js.map +0 -1
- package/dist/connectors/redshift-data.d.ts +0 -25
- package/dist/connectors/redshift-data.d.ts.map +0 -1
- package/dist/connectors/redshift-data.js +0 -237
- package/dist/connectors/redshift-data.js.map +0 -1
- package/dist/connectors/redshift.d.ts +0 -29
- package/dist/connectors/redshift.d.ts.map +0 -1
- package/dist/connectors/redshift.js +0 -141
- package/dist/connectors/redshift.js.map +0 -1
- package/dist/connectors/registry.d.ts +0 -7
- package/dist/connectors/registry.d.ts.map +0 -1
- package/dist/connectors/registry.js +0 -30
- package/dist/connectors/registry.js.map +0 -1
- package/dist/connectors/snowflake.d.ts +0 -37
- package/dist/connectors/snowflake.d.ts.map +0 -1
- package/dist/connectors/snowflake.js +0 -181
- package/dist/connectors/snowflake.js.map +0 -1
- package/dist/connectors/types.d.ts.map +0 -1
- package/dist/connectors/types.js +0 -15
- package/dist/connectors/types.js.map +0 -1
- package/dist/connectors/url-parsers.d.ts +0 -13
- package/dist/connectors/url-parsers.d.ts.map +0 -1
- package/dist/connectors/url-parsers.js +0 -95
- package/dist/connectors/url-parsers.js.map +0 -1
- package/dist/connectors/url.d.ts +0 -36
- package/dist/connectors/url.d.ts.map +0 -1
- package/dist/connectors/url.js +0 -139
- package/dist/connectors/url.js.map +0 -1
- package/dist/query/array-expansion.d.ts +0 -5
- package/dist/query/array-expansion.d.ts.map +0 -1
- package/dist/query/array-expansion.js +0 -39
- package/dist/query/array-expansion.js.map +0 -1
- package/dist/query/index.d.ts +0 -8
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -20
- package/dist/query/index.js.map +0 -1
- package/dist/query/pipeline.d.ts +0 -27
- package/dist/query/pipeline.d.ts.map +0 -1
- package/dist/query/pipeline.js +0 -49
- package/dist/query/pipeline.js.map +0 -1
- package/dist/query/strategy/http.d.ts +0 -7
- package/dist/query/strategy/http.d.ts.map +0 -1
- package/dist/query/strategy/http.js +0 -26
- package/dist/query/strategy/http.js.map +0 -1
- package/dist/query/strategy/index.d.ts +0 -4
- package/dist/query/strategy/index.d.ts.map +0 -1
- package/dist/query/strategy/index.js +0 -32
- package/dist/query/strategy/index.js.map +0 -1
- package/dist/query/strategy/sql.d.ts +0 -7
- package/dist/query/strategy/sql.d.ts.map +0 -1
- package/dist/query/strategy/sql.js +0 -40
- package/dist/query/strategy/sql.js.map +0 -1
- package/dist/query/strategy/types.d.ts +0 -6
- package/dist/query/strategy/types.d.ts.map +0 -1
- package/dist/query/strategy/types.js.map +0 -1
- package/dist/query/template.d.ts +0 -7
- package/dist/query/template.d.ts.map +0 -1
- package/dist/query/template.js +0 -43
- package/dist/query/template.js.map +0 -1
- package/dist/startup/index.d.ts +0 -5
- package/dist/startup/index.d.ts.map +0 -1
- package/dist/startup/index.js +0 -16
- package/dist/startup/index.js.map +0 -1
- package/dist/startup/sentinel-check.d.ts +0 -12
- package/dist/startup/sentinel-check.d.ts.map +0 -1
- package/dist/startup/sentinel-check.js +0 -142
- package/dist/startup/sentinel-check.js.map +0 -1
- package/dist/startup/sql-lint.d.ts +0 -13
- package/dist/startup/sql-lint.d.ts.map +0 -1
- package/dist/startup/sql-lint.js +0 -87
- package/dist/startup/sql-lint.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/query/pipeline.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EAGZ,MAAM,qBAAqB,CAAC;AAI7B,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAC9D,CAAC;IACF,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CAChE;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,UAAU,EACjB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,WAAW,CAAC,CAmCtB"}
|
package/dist/query/pipeline.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
import { getStrategy } from "./strategy";
|
|
15
|
-
const SORT_DIRECTION_ALLOWLIST = new Set(["ASC", "DESC", "asc", "desc"]);
|
|
16
|
-
/**
|
|
17
|
-
* Orchestrates query execution via the appropriate strategy:
|
|
18
|
-
* - SQL datasources: template rendering -> array expansion -> connector.query()
|
|
19
|
-
* - URL datasources: path + params -> connector.query()
|
|
20
|
-
*/
|
|
21
|
-
export async function executePanelQuery(query, resolvedParams, connector) {
|
|
22
|
-
// 0. Validate format-constrained param bindings (e.g. format: "sort_direction")
|
|
23
|
-
for (const [paramName, binding] of Object.entries(query.params ?? {})) {
|
|
24
|
-
if (binding.format === "sort_direction") {
|
|
25
|
-
const value = resolvedParams[paramName];
|
|
26
|
-
if (value !== undefined && value !== null) {
|
|
27
|
-
if (!SORT_DIRECTION_ALLOWLIST.has(String(value))) {
|
|
28
|
-
throw new Error(`Invalid sort_direction value "${value}" for param "${paramName}". Must be one of: ASC, DESC, asc, desc`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const strategy = getStrategy(connector.type);
|
|
34
|
-
const request = strategy.prepare(query, resolvedParams);
|
|
35
|
-
// For URL queries, pass panel-level columns via options so the connector can use them for parsing
|
|
36
|
-
const options = query.columns
|
|
37
|
-
? {
|
|
38
|
-
panelColumns: query.columns.map((c) => ({
|
|
39
|
-
name: c.name,
|
|
40
|
-
type: (c.type === "integer"
|
|
41
|
-
? "number"
|
|
42
|
-
: c.type),
|
|
43
|
-
nullable: c.nullable ?? true,
|
|
44
|
-
})),
|
|
45
|
-
}
|
|
46
|
-
: undefined;
|
|
47
|
-
return strategy.execute(connector, request, options);
|
|
48
|
-
}
|
|
49
|
-
//# sourceMappingURL=pipeline.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/query/pipeline.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAQzC,MAAM,wBAAwB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AAazE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAiB,EACjB,cAAuC,EACvC,SAAoB;IAEpB,gFAAgF;IAChF,KAAK,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QACtE,IAAI,OAAO,CAAC,MAAM,KAAK,gBAAgB,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;YACxC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAC1C,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACjD,MAAM,IAAI,KAAK,CACb,iCAAiC,KAAK,gBAAgB,SAAS,yCAAyC,CACzG,CAAC;gBACJ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAC9B,KAAgC,EAChC,cAAc,CACf,CAAC;IAEF,kGAAkG;IAClG,MAAM,OAAO,GAA6B,KAAK,CAAC,OAAO;QACrD,CAAC,CAAC;YACE,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACtC,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS;oBACzB,CAAC,CAAC,QAAQ;oBACV,CAAC,CAAC,CAAC,CAAC,IAAI,CAAuB;gBACjC,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,IAAI;aAC7B,CAAC,CAAC;SACJ;QACH,CAAC,CAAC,SAAS,CAAC;IAEd,OAAO,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { QueryStrategy } from "./types";
|
|
2
|
-
import type { QueryRequest, Connector, QueryResult, QueryOptions } from "../../connectors/types";
|
|
3
|
-
export declare class HttpStrategy implements QueryStrategy {
|
|
4
|
-
prepare(query: Record<string, unknown>, resolvedParams: Record<string, unknown>): QueryRequest;
|
|
5
|
-
execute(connector: Connector, request: QueryRequest, options?: QueryOptions): Promise<QueryResult>;
|
|
6
|
-
}
|
|
7
|
-
//# sourceMappingURL=http.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/query/strategy/http.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,WAAW,EACX,YAAY,EACb,MAAM,wBAAwB,CAAC;AAEhC,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CACL,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,YAAY;IAUT,OAAO,CACX,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,WAAW,CAAC;CAGxB"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
export class HttpStrategy {
|
|
15
|
-
prepare(query, resolvedParams) {
|
|
16
|
-
const path = query.path;
|
|
17
|
-
if (!path) {
|
|
18
|
-
throw new Error("HttpStrategy requires a 'path' field in the query definition");
|
|
19
|
-
}
|
|
20
|
-
return { type: "http", path, params: { ...resolvedParams } };
|
|
21
|
-
}
|
|
22
|
-
async execute(connector, request, options) {
|
|
23
|
-
return connector.query(request, options);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
//# sourceMappingURL=http.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/query/strategy/http.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AAUjC,MAAM,OAAO,YAAY;IACvB,OAAO,CACL,KAA8B,EAC9B,cAAuC;QAEvC,MAAM,IAAI,GAAG,KAAK,CAAC,IAA0B,CAAC;QAC9C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CACb,8DAA8D,CAC/D,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,cAAc,EAAE,EAAE,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,OAAO,CACX,SAAoB,EACpB,OAAqB,EACrB,OAAsB;QAEtB,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;CACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/query/strategy/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAO7C,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,GAAG,aAAa,CAa/D"}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
import { SqlStrategy } from "./sql";
|
|
15
|
-
import { HttpStrategy } from "./http";
|
|
16
|
-
const sqlStrategy = new SqlStrategy();
|
|
17
|
-
const httpStrategy = new HttpStrategy();
|
|
18
|
-
export function getStrategy(type) {
|
|
19
|
-
switch (type) {
|
|
20
|
-
case "postgres":
|
|
21
|
-
case "bigquery":
|
|
22
|
-
case "redshift":
|
|
23
|
-
case "redshift-data":
|
|
24
|
-
case "snowflake":
|
|
25
|
-
return sqlStrategy;
|
|
26
|
-
case "url":
|
|
27
|
-
return httpStrategy;
|
|
28
|
-
default:
|
|
29
|
-
throw new Error(`No strategy for datasource type: ${type}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/query/strategy/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AAIjC,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;AACtC,MAAM,YAAY,GAAG,IAAI,YAAY,EAAE,CAAC;AAExC,MAAM,UAAU,WAAW,CAAC,IAAoB;IAC9C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC;QAChB,KAAK,UAAU,CAAC;QAChB,KAAK,UAAU,CAAC;QAChB,KAAK,eAAe,CAAC;QACrB,KAAK,WAAW;YACd,OAAO,WAAW,CAAC;QACrB,KAAK,KAAK;YACR,OAAO,YAAY,CAAC;QACtB;YACE,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { QueryStrategy } from "./types";
|
|
2
|
-
import type { QueryRequest, Connector, QueryResult, QueryOptions } from "../../connectors/types";
|
|
3
|
-
export declare class SqlStrategy implements QueryStrategy {
|
|
4
|
-
prepare(query: Record<string, unknown>, resolvedParams: Record<string, unknown>): QueryRequest;
|
|
5
|
-
execute(connector: Connector, request: QueryRequest, options?: QueryOptions): Promise<QueryResult>;
|
|
6
|
-
}
|
|
7
|
-
//# sourceMappingURL=sql.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../src/query/strategy/sql.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,WAAW,EACX,YAAY,EACb,MAAM,wBAAwB,CAAC;AAIhC,qBAAa,WAAY,YAAW,aAAa;IAC/C,OAAO,CACL,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,YAAY;IAgCT,OAAO,CACX,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,WAAW,CAAC;CAGxB"}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
import { renderTemplate } from "../template";
|
|
15
|
-
import { expandArrayParams } from "../array-expansion";
|
|
16
|
-
export class SqlStrategy {
|
|
17
|
-
prepare(query, resolvedParams) {
|
|
18
|
-
const sql = query.sql;
|
|
19
|
-
if (!sql) {
|
|
20
|
-
throw new Error("SqlStrategy requires a 'sql' field in the query definition");
|
|
21
|
-
}
|
|
22
|
-
const templateVars = (query.template_vars ?? {});
|
|
23
|
-
const resolvedVarValues = {};
|
|
24
|
-
for (const [name, def] of Object.entries(templateVars)) {
|
|
25
|
-
if (def.from_filter && resolvedParams[def.from_filter] !== undefined) {
|
|
26
|
-
resolvedVarValues[name] = String(resolvedParams[def.from_filter]);
|
|
27
|
-
}
|
|
28
|
-
else if (def.value) {
|
|
29
|
-
resolvedVarValues[name] = def.value;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const renderedSql = renderTemplate(sql, templateVars, resolvedVarValues, resolvedParams);
|
|
33
|
-
const expanded = expandArrayParams(renderedSql, resolvedParams);
|
|
34
|
-
return { type: "sql", sql: expanded.sql, params: expanded.params };
|
|
35
|
-
}
|
|
36
|
-
async execute(connector, request, options) {
|
|
37
|
-
return connector.query(request, options);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=sql.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sql.js","sourceRoot":"","sources":["../../../src/query/strategy/sql.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AASjC,OAAO,EAAE,cAAc,EAAoB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,OAAO,WAAW;IACtB,OAAO,CACL,KAA8B,EAC9B,cAAuC;QAEvC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAyB,CAAC;QAC5C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CACb,4DAA4D,CAC7D,CAAC;QACJ,CAAC;QACD,MAAM,YAAY,GAAG,CAAC,KAAK,CAAC,aAAa,IAAI,EAAE,CAG9C,CAAC;QAEF,MAAM,iBAAiB,GAA2B,EAAE,CAAC;QACrD,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YACvD,IAAI,GAAG,CAAC,WAAW,IAAI,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,SAAS,EAAE,CAAC;gBACrE,iBAAiB,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;YACpE,CAAC;iBAAM,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;gBACrB,iBAAiB,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC;YACtC,CAAC;QACH,CAAC;QAED,MAAM,WAAW,GAAG,cAAc,CAChC,GAAG,EACH,YAAY,EACZ,iBAAiB,EACjB,cAAc,CACf,CAAC;QACF,MAAM,QAAQ,GAAG,iBAAiB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QAEhE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IACrE,CAAC;IAED,KAAK,CAAC,OAAO,CACX,SAAoB,EACpB,OAAqB,EACrB,OAAsB;QAEtB,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;CACF"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { QueryRequest, Connector, QueryResult, QueryOptions } from "../../connectors/types";
|
|
2
|
-
export interface QueryStrategy {
|
|
3
|
-
prepare(query: Record<string, unknown>, resolvedParams: Record<string, unknown>): QueryRequest;
|
|
4
|
-
execute(connector: Connector, request: QueryRequest, options?: QueryOptions): Promise<QueryResult>;
|
|
5
|
-
}
|
|
6
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/query/strategy/types.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,WAAW,EACX,YAAY,EACb,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,aAAa;IAC5B,OAAO,CACL,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,YAAY,CAAC;IAChB,OAAO,CACL,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,WAAW,CAAC,CAAC;CACzB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/query/strategy/types.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC"}
|
package/dist/query/template.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export interface TemplateVar {
|
|
2
|
-
allowlist: string[];
|
|
3
|
-
from_filter?: string;
|
|
4
|
-
value?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function renderTemplate(sql: string, templateVars: Record<string, TemplateVar>, resolvedVarValues: Record<string, string>, params: Record<string, unknown>): string;
|
|
7
|
-
//# sourceMappingURL=template.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/query/template.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAgCR"}
|
package/dist/query/template.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
import nunjucks from "nunjucks";
|
|
15
|
-
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
16
|
-
const PARAMS_INTERPOLATION_RE = /\{\{-?\s*params\./;
|
|
17
|
-
export function renderTemplate(sql, templateVars, resolvedVarValues, params) {
|
|
18
|
-
// Guard: reject direct param interpolation — this bypasses parameterization
|
|
19
|
-
if (PARAMS_INTERPOLATION_RE.test(sql)) {
|
|
20
|
-
throw new Error("SQL template directly interpolates a query parameter via {{ params.X }}. " +
|
|
21
|
-
"This bypasses parameterized queries and is a SQL injection risk. " +
|
|
22
|
-
"Use a bound parameter (:param_name) in the SQL and declare it under query.params instead.");
|
|
23
|
-
}
|
|
24
|
-
// 1. Validate each template var value against its allowlist
|
|
25
|
-
for (const [name, def] of Object.entries(templateVars)) {
|
|
26
|
-
const value = resolvedVarValues[name];
|
|
27
|
-
if (value === undefined && def.value) {
|
|
28
|
-
// Use the hardcoded default value
|
|
29
|
-
resolvedVarValues[name] = def.value;
|
|
30
|
-
}
|
|
31
|
-
const finalValue = resolvedVarValues[name];
|
|
32
|
-
if (finalValue !== undefined && !def.allowlist.includes(finalValue)) {
|
|
33
|
-
throw new Error(`Template variable '${name}' value '${finalValue}' is not in the allowlist: [${def.allowlist.join(", ")}]`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// 2. Render with Nunjucks -- context contains only declared template vars and params
|
|
37
|
-
const context = {
|
|
38
|
-
...resolvedVarValues,
|
|
39
|
-
params, // for {% if params.x %} conditional blocks
|
|
40
|
-
};
|
|
41
|
-
return env.renderString(sql, context);
|
|
42
|
-
}
|
|
43
|
-
//# sourceMappingURL=template.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/query/template.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AAEjC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;AAElE,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAQpD,MAAM,UAAU,cAAc,CAC5B,GAAW,EACX,YAAyC,EACzC,iBAAyC,EACzC,MAA+B;IAE/B,4EAA4E;IAC5E,IAAI,uBAAuB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,2EAA2E;YACzE,mEAAmE;YACnE,2FAA2F,CAC9F,CAAC;IACJ,CAAC;IAED,4DAA4D;IAC5D,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACrC,kCAAkC;YAClC,iBAAiB,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC;QACtC,CAAC;QACD,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,UAAU,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,KAAK,CACb,sBAAsB,IAAI,YAAY,UAAU,+BAA+B,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC3G,CAAC;QACJ,CAAC;IACH,CAAC;IAED,qFAAqF;IACrF,MAAM,OAAO,GAAG;QACd,GAAG,iBAAiB;QACpB,MAAM,EAAE,2CAA2C;KACpD,CAAC;IAEF,OAAO,GAAG,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACxC,CAAC"}
|
package/dist/startup/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/startup/index.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,YAAY,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/startup/index.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
export { lintDashboardQueries } from "./sql-lint";
|
|
15
|
-
export { checkParamInterpolation } from "./sentinel-check";
|
|
16
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/startup/index.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AAEjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { Dashboard } from "../schemas/dashboard";
|
|
2
|
-
export interface ParamInterpolationViolation {
|
|
3
|
-
slug: string;
|
|
4
|
-
panelId: string;
|
|
5
|
-
paramName: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Check all panel SQL queries for direct param interpolation.
|
|
9
|
-
* Returns one violation per (slug, panelId, paramName) combination found.
|
|
10
|
-
*/
|
|
11
|
-
export declare function checkParamInterpolation(dashboards: Map<string, Dashboard>): ParamInterpolationViolation[];
|
|
12
|
-
//# sourceMappingURL=sentinel-check.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sentinel-check.d.ts","sourceRoot":"","sources":["../../src/startup/sentinel-check.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,SAAS,EAAuB,MAAM,sBAAsB,CAAC;AAI3E,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AA8CD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACjC,2BAA2B,EAAE,CA0E/B"}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
/**
|
|
15
|
-
* Sentinel-based param interpolation check.
|
|
16
|
-
*
|
|
17
|
-
* For each panel query that has params, this check injects symbol-only sentinel
|
|
18
|
-
* values (immune to Nunjucks string filters like | upper / | lower) and renders
|
|
19
|
-
* the SQL template directly via Nunjucks. If any sentinel appears in the output,
|
|
20
|
-
* the template is directly interpolating a user-supplied parameter into SQL —
|
|
21
|
-
* a SQL injection risk.
|
|
22
|
-
*
|
|
23
|
-
* Symbol-only alphabet means sentinels survive filters like | upper, | lower,
|
|
24
|
-
* | title, | capitalize, | trim without transformation.
|
|
25
|
-
*
|
|
26
|
-
* Note: We render via Nunjucks directly (bypassing renderTemplate) so that the
|
|
27
|
-
* Task 1 guard — which throws on {{ params.X }} patterns — does not prevent
|
|
28
|
-
* detection. The sentinel check IS the detection mechanism for that pattern.
|
|
29
|
-
*/
|
|
30
|
-
import nunjucks from "nunjucks";
|
|
31
|
-
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
32
|
-
/** Alphabet of symbols only — immune to Nunjucks case/trim filters. */
|
|
33
|
-
const SENTINEL_ALPHABET = "@#$%^&!~";
|
|
34
|
-
const SENTINEL_LENGTH = 16;
|
|
35
|
-
function generateSentinel() {
|
|
36
|
-
let s = "";
|
|
37
|
-
for (let i = 0; i < SENTINEL_LENGTH; i++) {
|
|
38
|
-
s += SENTINEL_ALPHABET[Math.floor(Math.random() * SENTINEL_ALPHABET.length)];
|
|
39
|
-
}
|
|
40
|
-
return s;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Collect all panels from any dashboard layout:
|
|
44
|
-
* - top-level panels (classic)
|
|
45
|
-
* - section panels (classic with sections)
|
|
46
|
-
* - tab section panels (tabbed)
|
|
47
|
-
*/
|
|
48
|
-
function getAllPanels(dashboard) {
|
|
49
|
-
const panels = [];
|
|
50
|
-
for (const p of dashboard.panels ?? []) {
|
|
51
|
-
panels.push(p);
|
|
52
|
-
}
|
|
53
|
-
const d = dashboard;
|
|
54
|
-
for (const section of d.sections ?? []) {
|
|
55
|
-
for (const p of section.panels) {
|
|
56
|
-
panels.push(p);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
for (const tab of d.tabs ?? []) {
|
|
60
|
-
for (const section of tab.sections) {
|
|
61
|
-
for (const p of section.panels) {
|
|
62
|
-
panels.push(p);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return panels;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Check all panel SQL queries for direct param interpolation.
|
|
70
|
-
* Returns one violation per (slug, panelId, paramName) combination found.
|
|
71
|
-
*/
|
|
72
|
-
export function checkParamInterpolation(dashboards) {
|
|
73
|
-
const violations = [];
|
|
74
|
-
for (const [slug, dashboard] of dashboards) {
|
|
75
|
-
for (const panel of getAllPanels(dashboard)) {
|
|
76
|
-
if (!("query" in panel) || !panel.query)
|
|
77
|
-
continue;
|
|
78
|
-
const query = panel.query;
|
|
79
|
-
if (!("sql" in query) || typeof query.sql !== "string")
|
|
80
|
-
continue;
|
|
81
|
-
if (!("params" in query) || !query.params)
|
|
82
|
-
continue;
|
|
83
|
-
const params = query.params;
|
|
84
|
-
const paramNames = Object.keys(params);
|
|
85
|
-
if (paramNames.length === 0)
|
|
86
|
-
continue;
|
|
87
|
-
// Build sentinel map: paramName → unique sentinel value
|
|
88
|
-
const sentinelMap = new Map(); // paramName → sentinel
|
|
89
|
-
const sentinelToParam = new Map(); // sentinel → paramName
|
|
90
|
-
for (const paramName of paramNames) {
|
|
91
|
-
let sentinel;
|
|
92
|
-
do {
|
|
93
|
-
sentinel = generateSentinel();
|
|
94
|
-
} while (sentinelToParam.has(sentinel));
|
|
95
|
-
sentinelMap.set(paramName, sentinel);
|
|
96
|
-
sentinelToParam.set(sentinel, paramName);
|
|
97
|
-
}
|
|
98
|
-
// Build sentinel params object
|
|
99
|
-
const sentinelParams = {};
|
|
100
|
-
for (const [paramName, sentinel] of sentinelMap) {
|
|
101
|
-
sentinelParams[paramName] = sentinel;
|
|
102
|
-
}
|
|
103
|
-
// Stub template_vars with first allowlist value or default
|
|
104
|
-
const templateVars = ("template_vars" in query ? query.template_vars : undefined);
|
|
105
|
-
const stubVarValues = {};
|
|
106
|
-
let canRender = true;
|
|
107
|
-
for (const [varName, varDef] of Object.entries(templateVars ?? {})) {
|
|
108
|
-
const stubValue = varDef.allowlist[0] ?? varDef.value;
|
|
109
|
-
if (stubValue === undefined) {
|
|
110
|
-
canRender = false;
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
stubVarValues[varName] = stubValue;
|
|
114
|
-
}
|
|
115
|
-
if (!canRender)
|
|
116
|
-
continue;
|
|
117
|
-
// Render via Nunjucks directly — bypassing renderTemplate's guard so we
|
|
118
|
-
// can detect the interpolation pattern ourselves via sentinel scanning.
|
|
119
|
-
let rendered;
|
|
120
|
-
try {
|
|
121
|
-
rendered = env.renderString(query.sql, {
|
|
122
|
-
...stubVarValues,
|
|
123
|
-
params: sentinelParams,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// Unrelated Nunjucks syntax error — skip this panel.
|
|
128
|
-
// The runtime guard in renderTemplate or sql-lint will surface it.
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
// Scan rendered SQL for any sentinel
|
|
132
|
-
for (const [sentinel, paramName] of sentinelToParam) {
|
|
133
|
-
if (rendered.includes(sentinel)) {
|
|
134
|
-
violations.push({ slug, panelId: panel.id, paramName });
|
|
135
|
-
break; // one violation per panel is enough
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return violations;
|
|
141
|
-
}
|
|
142
|
-
//# sourceMappingURL=sentinel-check.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sentinel-check.js","sourceRoot":"","sources":["../../src/startup/sentinel-check.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Dashboard } from "../schemas/dashboard";
|
|
2
|
-
export interface SqlLintError {
|
|
3
|
-
dashboardSlug: string;
|
|
4
|
-
panelId: string;
|
|
5
|
-
error: string;
|
|
6
|
-
renderedSql: string;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Lint all panel queries across all dashboards.
|
|
10
|
-
* Returns an array of errors (empty = all clear).
|
|
11
|
-
*/
|
|
12
|
-
export declare function lintDashboardQueries(dashboards: Map<string, Dashboard>): SqlLintError[];
|
|
13
|
-
//# sourceMappingURL=sql-lint.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sql-lint.d.ts","sourceRoot":"","sources":["../../src/startup/sql-lint.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGtD,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACjC,YAAY,EAAE,CAgChB"}
|
package/dist/startup/sql-lint.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Copyright 2026 Mataki Labs LLC
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Business Source License 1.1 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// https://github.com/panoboard/panoboard/blob/main/LICENSE
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
/**
|
|
15
|
-
* Deep SQL lint — best-effort static analysis of panel queries.
|
|
16
|
-
*
|
|
17
|
-
* When `startup_checks.deep_sql_lint: true`, each dashboard panel query
|
|
18
|
-
* is rendered with the first allowlist value for each template var, then
|
|
19
|
-
* parsed with node-sql-parser to detect syntax errors.
|
|
20
|
-
*
|
|
21
|
-
* This is best-effort: not all dialect-specific SQL will parse correctly.
|
|
22
|
-
* Bind parameters (:name) are replaced with placeholder values before parsing.
|
|
23
|
-
*/
|
|
24
|
-
import nodeSqlParser from "node-sql-parser";
|
|
25
|
-
const { Parser } = nodeSqlParser;
|
|
26
|
-
import { renderTemplate } from "../query/template";
|
|
27
|
-
/**
|
|
28
|
-
* Lint all panel queries across all dashboards.
|
|
29
|
-
* Returns an array of errors (empty = all clear).
|
|
30
|
-
*/
|
|
31
|
-
export function lintDashboardQueries(dashboards) {
|
|
32
|
-
const parser = new Parser();
|
|
33
|
-
const errors = [];
|
|
34
|
-
for (const [slug, dashboard] of dashboards) {
|
|
35
|
-
for (const panel of dashboard.panels ?? []) {
|
|
36
|
-
if (!("query" in panel) || !panel.query)
|
|
37
|
-
continue;
|
|
38
|
-
const query = panel.query;
|
|
39
|
-
if (!("sql" in query) || !query.sql)
|
|
40
|
-
continue;
|
|
41
|
-
const renderedSql = renderSqlForLint(query.sql, "template_vars" in query
|
|
42
|
-
? query.template_vars
|
|
43
|
-
: undefined);
|
|
44
|
-
const sanitized = replaceBindParams(renderedSql);
|
|
45
|
-
try {
|
|
46
|
-
parser.astify(sanitized, { database: "PostgreSQL" });
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
errors.push({
|
|
50
|
-
dashboardSlug: slug,
|
|
51
|
-
panelId: panel.id,
|
|
52
|
-
error: err instanceof Error ? err.message : String(err),
|
|
53
|
-
renderedSql: sanitized,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return errors;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Render template vars using the first allowlist value for each.
|
|
62
|
-
*/
|
|
63
|
-
function renderSqlForLint(sql, templateVars) {
|
|
64
|
-
if (!templateVars || Object.keys(templateVars).length === 0) {
|
|
65
|
-
return sql;
|
|
66
|
-
}
|
|
67
|
-
const resolvedValues = {};
|
|
68
|
-
for (const [name, def] of Object.entries(templateVars)) {
|
|
69
|
-
resolvedValues[name] = def.value ?? def.allowlist[0] ?? name;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
return renderTemplate(sql, templateVars, resolvedValues, {});
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// If template rendering fails, return raw SQL
|
|
76
|
-
return sql;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Replace :bind_param placeholders with safe literal values
|
|
81
|
-
* so the SQL parser does not choke on them.
|
|
82
|
-
*/
|
|
83
|
-
function replaceBindParams(sql) {
|
|
84
|
-
// Replace :word tokens (but not :: type casts) with '1'
|
|
85
|
-
return sql.replace(/(?<!:):([a-zA-Z_]\w*)/g, "'1'");
|
|
86
|
-
}
|
|
87
|
-
//# sourceMappingURL=sql-lint.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sql-lint.js","sourceRoot":"","sources":["../../src/startup/sql-lint.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,+DAA+D;AAC/D,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AAEjC;;;;;;;;;GASG;AAEH,OAAO,aAAa,MAAM,iBAAiB,CAAC;AAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAoB,MAAM,mBAAmB,CAAC;AASrE;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAkC;IAElC,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAmB,EAAE,CAAC;IAElC,KAAK,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3C,KAAK,MAAM,KAAK,IAAI,SAAS,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;YAC3C,IAAI,CAAC,CAAC,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK;gBAAE,SAAS;YAClD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;YAC1B,IAAI,CAAC,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG;gBAAE,SAAS;YAE9C,MAAM,WAAW,GAAG,gBAAgB,CAClC,KAAK,CAAC,GAAG,EACT,eAAe,IAAI,KAAK;gBACtB,CAAC,CAAE,KAAK,CAAC,aAAyD;gBAClE,CAAC,CAAC,SAAS,CACd,CAAC;YACF,MAAM,SAAS,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;YAEjD,IAAI,CAAC;gBACH,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC;YACvD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC;oBACV,aAAa,EAAE,IAAI;oBACnB,OAAO,EAAE,KAAK,CAAC,EAAE;oBACjB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;oBACvD,WAAW,EAAE,SAAS;iBACvB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,gBAAgB,CACvB,GAAW,EACX,YAA0C;IAE1C,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5D,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,cAAc,GAA2B,EAAE,CAAC;IAClD,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QACvD,cAAc,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC/D,CAAC;IAED,IAAI,CAAC;QACH,OAAO,cAAc,CAAC,GAAG,EAAE,YAAY,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,8CAA8C;QAC9C,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,GAAW;IACpC,wDAAwD;IACxD,OAAO,GAAG,CAAC,OAAO,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;AACtD,CAAC"}
|