@servicelabsco/slabs-access-manager 1.0.69 → 1.0.71
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/access/controllers/report.view.controller.d.ts +30 -12
- package/dist/access/dtos/report.view.query.dto.d.ts +1 -0
- package/dist/access/dtos/report.view.query.dto.js +20 -0
- package/dist/access/dtos/report.view.query.dto.js.map +1 -1
- package/dist/access/entities/business.material.view.entity.d.ts +9 -0
- package/dist/access/entities/business.material.view.entity.js +44 -0
- package/dist/access/entities/business.material.view.entity.js.map +1 -0
- package/dist/access/entities/index.d.ts +1 -0
- package/dist/access/entities/index.js +1 -0
- package/dist/access/entities/index.js.map +1 -1
- package/dist/access/es6.classes.d.ts +10 -3
- package/dist/access/es6.classes.js +6 -0
- package/dist/access/es6.classes.js.map +1 -1
- package/dist/access/libraries/index.d.ts +1 -0
- package/dist/access/libraries/index.js +1 -0
- package/dist/access/libraries/index.js.map +1 -1
- package/dist/access/libraries/report.view.enablement.d.ts +11 -0
- package/dist/access/libraries/report.view.enablement.js +49 -0
- package/dist/access/libraries/report.view.enablement.js.map +1 -0
- package/dist/access/libraries/report.view.query.compiler.d.ts +35 -2
- package/dist/access/libraries/report.view.query.compiler.js +274 -46
- package/dist/access/libraries/report.view.query.compiler.js.map +1 -1
- package/dist/access/services/listing.preference.service.js +11 -1
- package/dist/access/services/listing.preference.service.js.map +1 -1
- package/dist/access/services/listing.service.d.ts +3 -1
- package/dist/access/services/listing.service.js +7 -6
- package/dist/access/services/listing.service.js.map +1 -1
- package/dist/access/services/report.view.service.d.ts +34 -12
- package/dist/access/services/report.view.service.js +80 -13
- package/dist/access/services/report.view.service.js.map +1 -1
- package/dist/access/subscribers/business.material.view.subscriber.d.ts +8 -0
- package/dist/{accessWorkflow/subscribers/performance.record.subscriber.js → access/subscribers/business.material.view.subscriber.js} +10 -13
- package/dist/access/subscribers/business.material.view.subscriber.js.map +1 -0
- package/dist/access/subscribers/index.d.ts +1 -0
- package/dist/access/subscribers/index.js +1 -0
- package/dist/access/subscribers/index.js.map +1 -1
- package/dist/accessWorkflow/dtos/index.d.ts +0 -1
- package/dist/accessWorkflow/dtos/index.js +0 -1
- package/dist/accessWorkflow/dtos/index.js.map +1 -1
- package/dist/accessWorkflow/entities/index.d.ts +0 -1
- package/dist/accessWorkflow/entities/index.js +0 -1
- package/dist/accessWorkflow/entities/index.js.map +1 -1
- package/dist/accessWorkflow/es6.classes.d.ts +3 -6
- package/dist/accessWorkflow/es6.classes.js +0 -8
- package/dist/accessWorkflow/es6.classes.js.map +1 -1
- package/dist/accessWorkflow/jobs/index.d.ts +0 -1
- package/dist/accessWorkflow/jobs/index.js +0 -1
- package/dist/accessWorkflow/jobs/index.js.map +1 -1
- package/dist/accessWorkflow/services/es6.jobs.service.d.ts +1 -3
- package/dist/accessWorkflow/services/es6.jobs.service.js +1 -5
- package/dist/accessWorkflow/services/es6.jobs.service.js.map +1 -1
- package/dist/accessWorkflow/subscribers/index.d.ts +0 -1
- package/dist/accessWorkflow/subscribers/index.js +0 -1
- package/dist/accessWorkflow/subscribers/index.js.map +1 -1
- package/dist/config/entity.constants.d.ts +0 -2
- package/dist/config/entity.constants.js +0 -2
- package/dist/config/entity.constants.js.map +1 -1
- package/dist/development/controllers/development.catalog.controller.d.ts +25 -0
- package/dist/development/controllers/development.catalog.controller.js +32 -0
- package/dist/development/controllers/development.catalog.controller.js.map +1 -1
- package/dist/development/dtos/material.view.catalog.attributes.dto.d.ts +16 -0
- package/dist/development/dtos/material.view.catalog.attributes.dto.js +63 -1
- package/dist/development/dtos/material.view.catalog.attributes.dto.js.map +1 -1
- package/dist/development/services/admin.report.view.service.d.ts +23 -0
- package/dist/development/services/admin.report.view.service.js +195 -13
- package/dist/development/services/admin.report.view.service.js.map +1 -1
- package/dist/mcp/admin/tools/admin.tool.registry.js +6 -0
- package/dist/mcp/admin/tools/admin.tool.registry.js.map +1 -1
- package/dist/mcp/admin/tools/catalog/admin.column.describe.tool.js +1 -1
- package/dist/mcp/admin/tools/catalog/admin.column.describe.tool.js.map +1 -1
- package/dist/mcp/admin/tools/catalog/admin.view.business.clear.tool.d.ts +18 -0
- package/dist/mcp/admin/tools/catalog/admin.view.business.clear.tool.js +27 -0
- package/dist/mcp/admin/tools/catalog/admin.view.business.clear.tool.js.map +1 -0
- package/dist/mcp/admin/tools/catalog/admin.view.business.set.tool.d.ts +20 -0
- package/dist/mcp/admin/tools/catalog/admin.view.business.set.tool.js +28 -0
- package/dist/mcp/admin/tools/catalog/admin.view.business.set.tool.js.map +1 -0
- package/dist/mcp/admin/tools/catalog/admin.view.businesses.tool.d.ts +17 -0
- package/dist/mcp/admin/tools/catalog/admin.view.businesses.tool.js +23 -0
- package/dist/mcp/admin/tools/catalog/admin.view.businesses.tool.js.map +1 -0
- package/dist/mcp/admin/tools/catalog/admin.view.columns.sync.tool.d.ts +3 -1
- package/dist/mcp/admin/tools/catalog/admin.view.columns.sync.tool.js +5 -1
- package/dist/mcp/admin/tools/catalog/admin.view.columns.sync.tool.js.map +1 -1
- package/dist/mcp/admin/tools/catalog/admin.view.update.tool.d.ts +1 -1
- package/dist/mcp/admin/tools/catalog/admin.view.update.tool.js +2 -2
- package/dist/mcp/admin/tools/catalog/admin.view.update.tool.js.map +1 -1
- package/dist/mcp/admin/tools/catalog/index.d.ts +3 -0
- package/dist/mcp/admin/tools/catalog/index.js +3 -0
- package/dist/mcp/admin/tools/catalog/index.js.map +1 -1
- package/dist/mcp/es6.classes.d.ts +4 -0
- package/dist/mcp/logging/mcp.call.log.interceptor.js +1 -0
- package/dist/mcp/logging/mcp.call.log.interceptor.js.map +1 -1
- package/dist/mcp/logging/sanitize.mcp.call.log.d.ts +25 -0
- package/dist/mcp/logging/sanitize.mcp.call.log.js +60 -1
- package/dist/mcp/logging/sanitize.mcp.call.log.js.map +1 -1
- package/dist/mcp/mentities/mcp.call.log.mentity.d.ts +1 -0
- package/dist/mcp/mentities/mcp.call.log.mentity.js +4 -0
- package/dist/mcp/mentities/mcp.call.log.mentity.js.map +1 -1
- package/dist/mcp/tools/view.describe.tool.d.ts +1 -1
- package/dist/mcp/tools/view.describe.tool.js +1 -1
- package/dist/mcp/tools/view.describe.tool.js.map +1 -1
- package/dist/mcp/tools/view.query.tool.d.ts +5 -1
- package/dist/mcp/tools/view.query.tool.js +11 -3
- package/dist/mcp/tools/view.query.tool.js.map +1 -1
- package/dist/migrations/1787174933444-CreateBusinessMaterialViewTable.d.ts +5 -0
- package/dist/migrations/1787174933444-CreateBusinessMaterialViewTable.js +21 -0
- package/dist/migrations/1787174933444-CreateBusinessMaterialViewTable.js.map +1 -0
- package/package.json +1 -1
- package/dist/accessWorkflow/dtos/performance.record.attributes.dto.d.ts +0 -3
- package/dist/accessWorkflow/dtos/performance.record.attributes.dto.js +0 -8
- package/dist/accessWorkflow/dtos/performance.record.attributes.dto.js.map +0 -1
- package/dist/accessWorkflow/entities/performance.record.entity.d.ts +0 -11
- package/dist/accessWorkflow/entities/performance.record.entity.js +0 -50
- package/dist/accessWorkflow/entities/performance.record.entity.js.map +0 -1
- package/dist/accessWorkflow/jobs/performance.record.job.d.ts +0 -7
- package/dist/accessWorkflow/jobs/performance.record.job.js +0 -29
- package/dist/accessWorkflow/jobs/performance.record.job.js.map +0 -1
- package/dist/accessWorkflow/subscribers/performance.record.subscriber.d.ts +0 -10
- package/dist/accessWorkflow/subscribers/performance.record.subscriber.js.map +0 -1
|
@@ -6,6 +6,7 @@ export declare const DEFAULT_ROW_CAP = 500;
|
|
|
6
6
|
export declare const OFFSET_CAP = 10000;
|
|
7
7
|
export declare const GROUP_BY_CAP = 3;
|
|
8
8
|
export declare const STATEMENT_TIMEOUT_MS = 2000;
|
|
9
|
+
export declare const RELATIVE_DATE_TOKENS: readonly ["@today", "@today-Nd", "@today+Nd", "@today-Nw", "@today+Nw", "@today-Nm", "@today+Nm", "@today-Ny", "@today+Ny", "@month_start", "@quarter_start", "@fy_start"];
|
|
9
10
|
export declare const REPORT_VIEW_GRAMMAR: {
|
|
10
11
|
readonly operators: readonly [{
|
|
11
12
|
readonly op: "eq";
|
|
@@ -48,10 +49,15 @@ export declare const REPORT_VIEW_GRAMMAR: {
|
|
|
48
49
|
readonly buckets: readonly ["day", "week", "month", "quarter", "year"];
|
|
49
50
|
readonly sort_dirs: readonly ["asc", "desc"];
|
|
50
51
|
readonly relative_dates: {
|
|
51
|
-
readonly tokens: readonly ["@today", "@today-Nd", "@today+Nd"];
|
|
52
|
+
readonly tokens: readonly ["@today", "@today-Nd", "@today+Nd", "@today-Nw", "@today+Nw", "@today-Nm", "@today+Nm", "@today-Ny", "@today+Ny", "@month_start", "@quarter_start", "@fy_start"];
|
|
52
53
|
readonly example: "@today-30d";
|
|
53
54
|
readonly timezone: "Asia/Kolkata";
|
|
54
|
-
readonly
|
|
55
|
+
readonly fy_anchor: "04-01";
|
|
56
|
+
readonly note: "Resolved server-side to an ISO calendar date in Asia/Kolkata. @fy_start is 1 April (Indian FY); in January–March that is last April. Do not compute today or period boundaries in the client.";
|
|
57
|
+
};
|
|
58
|
+
readonly caller: {
|
|
59
|
+
readonly tokens: readonly ["@me"];
|
|
60
|
+
readonly note: "Resolved server-side to the caller: user id on id-type columns, email on string columns. Incompatible columns return value_type_mismatch.";
|
|
55
61
|
};
|
|
56
62
|
readonly rails: {
|
|
57
63
|
readonly tenancy: "injected";
|
|
@@ -60,6 +66,13 @@ export declare const REPORT_VIEW_GRAMMAR: {
|
|
|
60
66
|
readonly offset_cap: 10000;
|
|
61
67
|
readonly statement_timeout_ms: 2000;
|
|
62
68
|
};
|
|
69
|
+
readonly join: {
|
|
70
|
+
readonly max: 1;
|
|
71
|
+
readonly cardinality: "many_to_one";
|
|
72
|
+
readonly columns: "<relationship>.<column>";
|
|
73
|
+
readonly semantics: "LEFT JOIN; unmatched rows yield nulls for <rel>.*";
|
|
74
|
+
readonly note: "Name a catalog-declared relationship from view_describe. Do not send ON conditions.";
|
|
75
|
+
};
|
|
63
76
|
};
|
|
64
77
|
export type ReportViewOp = (typeof OPS)[number];
|
|
65
78
|
export type ReportViewAggFn = (typeof AGG_FNS)[number];
|
|
@@ -86,6 +99,9 @@ export type CatalogView = {
|
|
|
86
99
|
grain?: string;
|
|
87
100
|
domain?: string;
|
|
88
101
|
tenancy_column: string;
|
|
102
|
+
user_scoped?: boolean;
|
|
103
|
+
user_column?: string;
|
|
104
|
+
view_kind?: 'view' | 'materialized';
|
|
89
105
|
row_cap: number;
|
|
90
106
|
last_run_time?: Date | string | null;
|
|
91
107
|
example_queries?: Array<{
|
|
@@ -94,6 +110,7 @@ export type CatalogView = {
|
|
|
94
110
|
}>;
|
|
95
111
|
suggested_visualizations?: unknown[];
|
|
96
112
|
columns: CatalogColumn[];
|
|
113
|
+
relationships?: CatalogRelationship[];
|
|
97
114
|
};
|
|
98
115
|
export type ReportViewFilter = {
|
|
99
116
|
column?: string;
|
|
@@ -113,7 +130,17 @@ export type ReportViewSort = {
|
|
|
113
130
|
by?: string;
|
|
114
131
|
dir?: string;
|
|
115
132
|
};
|
|
133
|
+
export type CatalogRelationship = {
|
|
134
|
+
name: string;
|
|
135
|
+
view: string;
|
|
136
|
+
on: Record<string, string>;
|
|
137
|
+
when?: Record<string, unknown>;
|
|
138
|
+
cardinality: string;
|
|
139
|
+
description?: string;
|
|
140
|
+
target?: CatalogView;
|
|
141
|
+
};
|
|
116
142
|
export type ReportViewDsl = {
|
|
143
|
+
join?: string | string[];
|
|
117
144
|
columns?: string[];
|
|
118
145
|
filters?: ReportViewFilter[];
|
|
119
146
|
group_by?: ReportViewGroupBy[];
|
|
@@ -140,6 +167,7 @@ export type CompiledQuery = {
|
|
|
140
167
|
grain?: string;
|
|
141
168
|
table_name: string;
|
|
142
169
|
refreshed_at: Date | string | null;
|
|
170
|
+
live?: boolean;
|
|
143
171
|
};
|
|
144
172
|
};
|
|
145
173
|
export declare class ReportViewQueryError extends Error {
|
|
@@ -148,6 +176,10 @@ export declare class ReportViewQueryError extends Error {
|
|
|
148
176
|
}
|
|
149
177
|
export type CompileOptions = {
|
|
150
178
|
now?: Date;
|
|
179
|
+
user?: {
|
|
180
|
+
id: number;
|
|
181
|
+
email?: string;
|
|
182
|
+
};
|
|
151
183
|
};
|
|
152
184
|
export declare function compileReportViewQuery(view: CatalogView, dsl: ReportViewDsl, businessId: number, options?: CompileOptions): CompiledQuery;
|
|
153
185
|
export declare function agentVisibleColumns(view: CatalogView): CatalogColumn[];
|
|
@@ -160,4 +192,5 @@ export declare const ReportViewQueryCompiler: {
|
|
|
160
192
|
quoteIdent: typeof quoteIdent;
|
|
161
193
|
resolveRelativeDate: typeof resolveRelativeDate;
|
|
162
194
|
nearest: typeof nearest;
|
|
195
|
+
RELATIVE_DATE_TOKENS: readonly ["@today", "@today-Nd", "@today+Nd", "@today-Nw", "@today+Nw", "@today-Nm", "@today+Nm", "@today-Ny", "@today+Ny", "@month_start", "@quarter_start", "@fy_start"];
|
|
163
196
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ReportViewQueryCompiler = exports.ReportViewQueryError = exports.REPORT_VIEW_GRAMMAR = exports.STATEMENT_TIMEOUT_MS = exports.GROUP_BY_CAP = exports.OFFSET_CAP = exports.DEFAULT_ROW_CAP = exports.SORT_DIRS = exports.BUCKETS = exports.AGG_FNS = exports.OPS = void 0;
|
|
3
|
+
exports.ReportViewQueryCompiler = exports.ReportViewQueryError = exports.REPORT_VIEW_GRAMMAR = exports.RELATIVE_DATE_TOKENS = exports.STATEMENT_TIMEOUT_MS = exports.GROUP_BY_CAP = exports.OFFSET_CAP = exports.DEFAULT_ROW_CAP = exports.SORT_DIRS = exports.BUCKETS = exports.AGG_FNS = exports.OPS = void 0;
|
|
4
4
|
exports.compileReportViewQuery = compileReportViewQuery;
|
|
5
5
|
exports.agentVisibleColumns = agentVisibleColumns;
|
|
6
6
|
exports.quoteIdent = quoteIdent;
|
|
@@ -16,8 +16,23 @@ exports.GROUP_BY_CAP = 3;
|
|
|
16
16
|
exports.STATEMENT_TIMEOUT_MS = 2000;
|
|
17
17
|
const IDENT = /^[a-z][a-z0-9_]*$/;
|
|
18
18
|
const NUMERIC_AGG = new Set(['sum', 'avg', 'min', 'max']);
|
|
19
|
-
const RELATIVE_DATE = /^@today(?:([+-])(\d+)
|
|
19
|
+
const RELATIVE_DATE = /^@(?:today(?:([+-])(\d+)([dwmy]))?|month_start|quarter_start|fy_start)$/;
|
|
20
20
|
const RELATIVE_DATE_TZ = 'Asia/Kolkata';
|
|
21
|
+
const FY_START_MONTH = 4;
|
|
22
|
+
exports.RELATIVE_DATE_TOKENS = [
|
|
23
|
+
'@today',
|
|
24
|
+
'@today-Nd',
|
|
25
|
+
'@today+Nd',
|
|
26
|
+
'@today-Nw',
|
|
27
|
+
'@today+Nw',
|
|
28
|
+
'@today-Nm',
|
|
29
|
+
'@today+Nm',
|
|
30
|
+
'@today-Ny',
|
|
31
|
+
'@today+Ny',
|
|
32
|
+
'@month_start',
|
|
33
|
+
'@quarter_start',
|
|
34
|
+
'@fy_start',
|
|
35
|
+
];
|
|
21
36
|
exports.REPORT_VIEW_GRAMMAR = {
|
|
22
37
|
operators: [
|
|
23
38
|
{ op: 'eq', value: 'scalar' },
|
|
@@ -40,10 +55,15 @@ exports.REPORT_VIEW_GRAMMAR = {
|
|
|
40
55
|
buckets: [...exports.BUCKETS],
|
|
41
56
|
sort_dirs: [...exports.SORT_DIRS],
|
|
42
57
|
relative_dates: {
|
|
43
|
-
tokens: [
|
|
58
|
+
tokens: [...exports.RELATIVE_DATE_TOKENS],
|
|
44
59
|
example: '@today-30d',
|
|
45
60
|
timezone: RELATIVE_DATE_TZ,
|
|
46
|
-
|
|
61
|
+
fy_anchor: '04-01',
|
|
62
|
+
note: 'Resolved server-side to an ISO calendar date in Asia/Kolkata. @fy_start is 1 April (Indian FY); in January–March that is last April. Do not compute today or period boundaries in the client.',
|
|
63
|
+
},
|
|
64
|
+
caller: {
|
|
65
|
+
tokens: ['@me'],
|
|
66
|
+
note: 'Resolved server-side to the caller: user id on id-type columns, email on string columns. Incompatible columns return value_type_mismatch.',
|
|
47
67
|
},
|
|
48
68
|
rails: {
|
|
49
69
|
tenancy: 'injected',
|
|
@@ -52,6 +72,13 @@ exports.REPORT_VIEW_GRAMMAR = {
|
|
|
52
72
|
offset_cap: exports.OFFSET_CAP,
|
|
53
73
|
statement_timeout_ms: exports.STATEMENT_TIMEOUT_MS,
|
|
54
74
|
},
|
|
75
|
+
join: {
|
|
76
|
+
max: 1,
|
|
77
|
+
cardinality: 'many_to_one',
|
|
78
|
+
columns: '<relationship>.<column>',
|
|
79
|
+
semantics: 'LEFT JOIN; unmatched rows yield nulls for <rel>.*',
|
|
80
|
+
note: 'Name a catalog-declared relationship from view_describe. Do not send ON conditions.',
|
|
81
|
+
},
|
|
55
82
|
};
|
|
56
83
|
class ReportViewQueryError extends Error {
|
|
57
84
|
constructor(code, message) {
|
|
@@ -68,7 +95,11 @@ function compileReportViewQuery(view, dsl, businessId, options = {}) {
|
|
|
68
95
|
const agentColumns = agentVisibleColumns(view);
|
|
69
96
|
const byName = new Map(agentColumns.map((column) => [column.name, column]));
|
|
70
97
|
const names = agentColumns.map((column) => column.name);
|
|
71
|
-
|
|
98
|
+
const join = resolveJoin(view, dsl.join);
|
|
99
|
+
const targetColumns = join?.target ? agentVisibleColumns(join.target) : [];
|
|
100
|
+
const targetByName = new Map(targetColumns.map((column) => [column.name, column]));
|
|
101
|
+
const targetNames = targetColumns.map((column) => column.name);
|
|
102
|
+
assertNoInjectedColumnLeak(dsl, view, names, join);
|
|
72
103
|
const aggregates = dsl.aggregates || [];
|
|
73
104
|
const groupBy = dsl.group_by || [];
|
|
74
105
|
const isAggregate = aggregates.length > 0 || groupBy.length > 0;
|
|
@@ -87,37 +118,47 @@ function compileReportViewQuery(view, dsl, businessId, options = {}) {
|
|
|
87
118
|
throw new ReportViewQueryError('limit', 'limit must be a positive integer.');
|
|
88
119
|
const limit = Math.min(requested, rowCap);
|
|
89
120
|
const now = options.now || new Date();
|
|
121
|
+
const user = options.user;
|
|
90
122
|
const params = [businessId];
|
|
123
|
+
const joinSql = join ? compileJoin(view, join, params, user) : '';
|
|
91
124
|
const select = [];
|
|
92
125
|
const output = [];
|
|
93
126
|
const selectNames = new Set();
|
|
94
|
-
const
|
|
127
|
+
const resolve = (name) => resolveColumn(name, view, join, byName, names, targetByName, targetNames);
|
|
128
|
+
const addOutput = (column, outputName = column.name) => {
|
|
95
129
|
output.push({
|
|
96
|
-
name:
|
|
97
|
-
title: column.display_name ||
|
|
130
|
+
name: outputName,
|
|
131
|
+
title: column.display_name || outputName,
|
|
98
132
|
type: column.semantic_type || 'string',
|
|
99
133
|
...(column.format ? { format: column.format } : {}),
|
|
100
134
|
...(column.enum_map ? { enum_map: column.enum_map } : {}),
|
|
101
135
|
});
|
|
102
|
-
selectNames.add(
|
|
136
|
+
selectNames.add(outputName);
|
|
137
|
+
};
|
|
138
|
+
const selectExpr = (column, bucket) => {
|
|
139
|
+
if (bucket)
|
|
140
|
+
return `date_trunc('${bucket}', ${column.sqlIdent}) AS ${quoteAlias(column.outputName)}`;
|
|
141
|
+
if (column.joined)
|
|
142
|
+
return `${column.sqlIdent} AS ${quoteAlias(column.outputName)}`;
|
|
143
|
+
return column.sqlIdent;
|
|
103
144
|
};
|
|
104
145
|
if (isAggregate) {
|
|
105
146
|
for (const group of groupBy) {
|
|
106
|
-
const column =
|
|
147
|
+
const column = resolve(group.column);
|
|
107
148
|
if (column.groupable === false) {
|
|
108
|
-
throw new ReportViewQueryError('not_groupable', `not_groupable: column '${column.
|
|
149
|
+
throw new ReportViewQueryError('not_groupable', `not_groupable: column '${column.outputName}' cannot be grouped.`);
|
|
109
150
|
}
|
|
110
151
|
if (group.bucket) {
|
|
111
152
|
if (column.bucketable !== true) {
|
|
112
|
-
throw new ReportViewQueryError('not_bucketable', `not_bucketable: column '${column.
|
|
153
|
+
throw new ReportViewQueryError('not_bucketable', `not_bucketable: column '${column.outputName}' is not bucketable.`);
|
|
113
154
|
}
|
|
114
155
|
const bucket = requireAllowlisted(group.bucket, exports.BUCKETS, 'bucket');
|
|
115
|
-
select.push(
|
|
156
|
+
select.push(selectExpr(column, bucket));
|
|
116
157
|
}
|
|
117
158
|
else {
|
|
118
|
-
select.push(
|
|
159
|
+
select.push(selectExpr(column));
|
|
119
160
|
}
|
|
120
|
-
addOutput(column);
|
|
161
|
+
addOutput(column, column.outputName);
|
|
121
162
|
}
|
|
122
163
|
for (const aggregate of aggregates) {
|
|
123
164
|
const fn = requireAllowlisted(aggregate.fn, exports.AGG_FNS, 'aggregate');
|
|
@@ -129,14 +170,17 @@ function compileReportViewQuery(view, dsl, businessId, options = {}) {
|
|
|
129
170
|
select.push(`COUNT(*) AS ${quoteIdent(alias)}`);
|
|
130
171
|
}
|
|
131
172
|
else {
|
|
132
|
-
const column =
|
|
173
|
+
const column = resolve(aggregate.column);
|
|
174
|
+
if (column.joined) {
|
|
175
|
+
throw new ReportViewQueryError('join_aggregate_not_supported', `join_aggregate_not_supported: aggregating joined column '${column.outputName}' is not supported. Aggregate on the base view; filter or group by <rel>.* instead.`);
|
|
176
|
+
}
|
|
133
177
|
assertAggregatable(column, fn);
|
|
134
178
|
if (fn === 'count')
|
|
135
|
-
select.push(`COUNT(${
|
|
179
|
+
select.push(`COUNT(${column.sqlIdent}) AS ${quoteIdent(alias)}`);
|
|
136
180
|
else if (fn === 'count_distinct')
|
|
137
|
-
select.push(`COUNT(DISTINCT ${
|
|
181
|
+
select.push(`COUNT(DISTINCT ${column.sqlIdent}) AS ${quoteIdent(alias)}`);
|
|
138
182
|
else
|
|
139
|
-
select.push(`${fn.toUpperCase()}(${
|
|
183
|
+
select.push(`${fn.toUpperCase()}(${column.sqlIdent}) AS ${quoteIdent(alias)}`);
|
|
140
184
|
}
|
|
141
185
|
addOutput({ name: alias, display_name: alias, semantic_type: 'number' });
|
|
142
186
|
}
|
|
@@ -150,24 +194,39 @@ function compileReportViewQuery(view, dsl, businessId, options = {}) {
|
|
|
150
194
|
else {
|
|
151
195
|
const requestedCols = dsl.columns?.length ? dsl.columns : names;
|
|
152
196
|
for (const name of requestedCols) {
|
|
153
|
-
const column =
|
|
154
|
-
select.push(
|
|
155
|
-
addOutput(column);
|
|
197
|
+
const column = resolve(name);
|
|
198
|
+
select.push(selectExpr(column));
|
|
199
|
+
addOutput(column, column.outputName);
|
|
156
200
|
}
|
|
157
201
|
if (!select.length) {
|
|
158
202
|
throw new ReportViewQueryError('unknown_column', `unknown_column: view '${view.catalog_name}' has no selectable columns.`);
|
|
159
203
|
}
|
|
160
204
|
}
|
|
161
|
-
const
|
|
205
|
+
const tenancyIdent = join ? `${quoteIdent(view.table_name)}.${quoteIdent(view.tenancy_column)}` : quoteIdent(view.tenancy_column);
|
|
206
|
+
const where = [`${tenancyIdent} = $1`];
|
|
207
|
+
if (view.user_scoped) {
|
|
208
|
+
const userCol = view.user_column;
|
|
209
|
+
if (!userCol || !IDENT.test(userCol)) {
|
|
210
|
+
throw new ReportViewQueryError('user_scope', 'user_scope: user_scoped views require a hidden user_column.');
|
|
211
|
+
}
|
|
212
|
+
if (!user?.id) {
|
|
213
|
+
throw new ReportViewQueryError('user_scope', 'user_scope: user_scoped views require an authenticated caller.');
|
|
214
|
+
}
|
|
215
|
+
const userIdent = join ? `${quoteIdent(view.table_name)}.${quoteIdent(userCol)}` : quoteIdent(userCol);
|
|
216
|
+
where.push(`${userIdent} = ${bind(params, user.id)}`);
|
|
217
|
+
}
|
|
162
218
|
for (const filter of dsl.filters || []) {
|
|
163
|
-
const column =
|
|
219
|
+
const column = resolve(filter.column);
|
|
164
220
|
if (column.filterable === false) {
|
|
165
|
-
throw new ReportViewQueryError('not_filterable', `not_filterable: column '${column.
|
|
221
|
+
throw new ReportViewQueryError('not_filterable', `not_filterable: column '${column.outputName}' cannot be filtered.`);
|
|
166
222
|
}
|
|
167
223
|
const op = requireAllowlisted(filter.op, exports.OPS, 'op');
|
|
168
|
-
where.push(compileFilter(column, op, filter.value, params, now));
|
|
224
|
+
where.push(compileFilter(column, column.sqlIdent, op, filter.value, params, now, user));
|
|
169
225
|
}
|
|
170
|
-
const sql = [`SELECT ${select.join(', ')}`, `FROM ${quoteIdent(view.table_name)}
|
|
226
|
+
const sql = [`SELECT ${select.join(', ')}`, `FROM ${quoteIdent(view.table_name)}`];
|
|
227
|
+
if (joinSql)
|
|
228
|
+
sql.push(joinSql);
|
|
229
|
+
sql.push(`WHERE ${where.join(' AND ')}`);
|
|
171
230
|
if (groupBy.length) {
|
|
172
231
|
sql.push(`GROUP BY ${groupBy.map((_, index) => String(index + 1)).join(', ')}`);
|
|
173
232
|
}
|
|
@@ -178,7 +237,7 @@ function compileReportViewQuery(view, dsl, businessId, options = {}) {
|
|
|
178
237
|
throw unknownColumn(by, [...selectNames], view.catalog_name);
|
|
179
238
|
}
|
|
180
239
|
const dir = item.dir ? requireAllowlisted(String(item.dir).toLowerCase(), exports.SORT_DIRS, 'dir') : 'asc';
|
|
181
|
-
return `${
|
|
240
|
+
return `${quoteAlias(by)} ${dir.toUpperCase()}`;
|
|
182
241
|
});
|
|
183
242
|
sql.push(`ORDER BY ${order.join(', ')}`);
|
|
184
243
|
}
|
|
@@ -195,7 +254,8 @@ function compileReportViewQuery(view, dsl, businessId, options = {}) {
|
|
|
195
254
|
name: view.catalog_name,
|
|
196
255
|
grain: view.grain,
|
|
197
256
|
table_name: view.table_name,
|
|
198
|
-
refreshed_at: view.last_run_time ?? null,
|
|
257
|
+
refreshed_at: view.view_kind === 'view' ? null : (view.last_run_time ?? null),
|
|
258
|
+
...(view.view_kind === 'view' ? { live: true } : {}),
|
|
199
259
|
},
|
|
200
260
|
};
|
|
201
261
|
}
|
|
@@ -203,6 +263,8 @@ function agentVisibleColumns(view) {
|
|
|
203
263
|
return (view.columns || []).filter((column) => {
|
|
204
264
|
if (!column?.name || column.name === view.tenancy_column || column.name === 'business_id')
|
|
205
265
|
return false;
|
|
266
|
+
if (view.user_column && column.name === view.user_column)
|
|
267
|
+
return false;
|
|
206
268
|
if (column.visibility === false)
|
|
207
269
|
return false;
|
|
208
270
|
if (column.sensitive === true)
|
|
@@ -215,17 +277,119 @@ function quoteIdent(name) {
|
|
|
215
277
|
throw new ReportViewQueryError('unknown_column', `unknown_column: '${name}' is not a valid identifier.`);
|
|
216
278
|
return `"${name}"`;
|
|
217
279
|
}
|
|
218
|
-
function
|
|
280
|
+
function quoteAlias(name) {
|
|
281
|
+
if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/.test(name)) {
|
|
282
|
+
throw new ReportViewQueryError('unknown_column', `unknown_column: '${name}' is not a valid identifier.`);
|
|
283
|
+
}
|
|
284
|
+
return `"${name}"`;
|
|
285
|
+
}
|
|
286
|
+
function declaredRelationshipNames(view) {
|
|
287
|
+
return (view.relationships || []).map((relationship) => relationship.name).filter(Boolean);
|
|
288
|
+
}
|
|
289
|
+
function resolveJoin(view, joinName) {
|
|
290
|
+
const name = normalizeJoinName(joinName);
|
|
291
|
+
if (name === undefined)
|
|
292
|
+
return undefined;
|
|
293
|
+
const declared = declaredRelationshipNames(view);
|
|
294
|
+
const listed = declared.length ? declared.join(', ') : '(none)';
|
|
295
|
+
const shown = IDENT.test(name) ? `'${name}'` : 'that name';
|
|
296
|
+
if (!IDENT.test(name)) {
|
|
297
|
+
throw new ReportViewQueryError('unknown_relationship', `unknown_relationship: ${shown} is not a declared relationship on ${view.catalog_name}. Declared: ${listed}.`);
|
|
298
|
+
}
|
|
299
|
+
const relationship = (view.relationships || []).find((candidate) => candidate.name === name);
|
|
300
|
+
if (!relationship) {
|
|
301
|
+
throw new ReportViewQueryError('unknown_relationship', `unknown_relationship: '${name}' is not a declared relationship on ${view.catalog_name}. Declared: ${listed}.`);
|
|
302
|
+
}
|
|
303
|
+
if (relationship.cardinality !== 'many_to_one' || !relationship.target) {
|
|
304
|
+
throw new ReportViewQueryError('relationship_unavailable', `relationship_unavailable: relationship '${relationship.name}' on ${view.catalog_name} is not available (target view '${relationship.view}' is not agent-visible or failed the catalog gate).`);
|
|
305
|
+
}
|
|
306
|
+
return relationship;
|
|
307
|
+
}
|
|
308
|
+
function normalizeJoinName(join) {
|
|
309
|
+
if (join === undefined || join === '')
|
|
310
|
+
return undefined;
|
|
311
|
+
if (Array.isArray(join)) {
|
|
312
|
+
if (!join.length)
|
|
313
|
+
return undefined;
|
|
314
|
+
if (join.length > 1) {
|
|
315
|
+
throw new ReportViewQueryError('join_limit', `join_limit: v1 supports exactly one relationship (got ${join.length}).`);
|
|
316
|
+
}
|
|
317
|
+
return join[0];
|
|
318
|
+
}
|
|
319
|
+
return join;
|
|
320
|
+
}
|
|
321
|
+
function compileJoin(view, relationship, params, user) {
|
|
322
|
+
const alias = quoteIdent(relationship.name);
|
|
323
|
+
const target = relationship.target;
|
|
324
|
+
const on = Object.entries(relationship.on || {});
|
|
325
|
+
if (!on.length) {
|
|
326
|
+
throw new ReportViewQueryError('relationship_unavailable', `relationship_unavailable: relationship '${relationship.name}' on ${view.catalog_name} is not available (target view '${relationship.view}' is not agent-visible or failed the catalog gate).`);
|
|
327
|
+
}
|
|
328
|
+
const conds = [];
|
|
329
|
+
for (const [sourceCol, targetCol] of on) {
|
|
330
|
+
conds.push(`${quoteIdent(view.table_name)}.${quoteIdent(sourceCol)} = ${alias}.${quoteIdent(String(targetCol))}`);
|
|
331
|
+
}
|
|
332
|
+
conds.push(`${alias}.${quoteIdent(target.tenancy_column || 'business_id')} = $1`);
|
|
333
|
+
if (target.user_scoped) {
|
|
334
|
+
const userCol = target.user_column;
|
|
335
|
+
if (!userCol || !IDENT.test(userCol) || !user?.id) {
|
|
336
|
+
throw new ReportViewQueryError('user_scope', `user_scope: joined view '${relationship.view}' is user-scoped and requires a hidden user_column plus an authenticated caller.`);
|
|
337
|
+
}
|
|
338
|
+
conds.push(`${alias}.${quoteIdent(userCol)} = ${bind(params, user.id)}`);
|
|
339
|
+
}
|
|
340
|
+
const byName = new Map((view.columns || []).map((column) => [column.name, column]));
|
|
341
|
+
for (const [sourceCol, value] of Object.entries(relationship.when || {})) {
|
|
342
|
+
if (sourceCol === view.tenancy_column || sourceCol === 'business_id')
|
|
343
|
+
continue;
|
|
344
|
+
const column = byName.get(sourceCol);
|
|
345
|
+
if (!column)
|
|
346
|
+
throw unknownColumn(sourceCol, [...byName.keys()], view.catalog_name);
|
|
347
|
+
conds.push(`${quoteIdent(view.table_name)}.${quoteIdent(sourceCol)} = ${bind(params, mapEnum(column, value))}`);
|
|
348
|
+
}
|
|
349
|
+
return `LEFT JOIN ${quoteIdent(target.table_name)} AS ${alias} ON ${conds.join(' AND ')}`;
|
|
350
|
+
}
|
|
351
|
+
function resolveColumn(name, view, join, byName, names, targetByName, targetNames) {
|
|
219
352
|
const key = String(name || '');
|
|
220
|
-
|
|
221
|
-
|
|
353
|
+
const parts = key.split('.');
|
|
354
|
+
if (parts.length > 2)
|
|
355
|
+
throw unknownColumn(key, names, view.catalog_name);
|
|
356
|
+
if (parts.length === 2) {
|
|
357
|
+
const [relName, columnName] = parts;
|
|
358
|
+
if (!join || join.name !== relName) {
|
|
359
|
+
const hint = join
|
|
360
|
+
? ` Active join is '${join.name}'; address joined columns as ${join.name}.<column>.`
|
|
361
|
+
: ' Name a catalog-declared relationship with join first.';
|
|
362
|
+
throw new ReportViewQueryError('unknown_column', `unknown_column: '${key}' is not a column on ${view.catalog_name}.${hint}`);
|
|
363
|
+
}
|
|
364
|
+
if (columnName === 'business_id' || columnName === join.target?.tenancy_column || columnName === join.target?.user_column) {
|
|
365
|
+
throw unknownColumn(key, targetNames, join.view);
|
|
366
|
+
}
|
|
367
|
+
const column = targetByName.get(columnName);
|
|
368
|
+
if (!column)
|
|
369
|
+
throw unknownColumn(key, targetNames, join.view);
|
|
370
|
+
return {
|
|
371
|
+
...column,
|
|
372
|
+
sqlIdent: `${quoteIdent(join.name)}.${quoteIdent(column.name)}`,
|
|
373
|
+
outputName: `${join.name}.${column.name}`,
|
|
374
|
+
joined: true,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (key === 'business_id' || key === view.tenancy_column || (view.user_column && key === view.user_column)) {
|
|
378
|
+
throw unknownColumn(key, names, view.catalog_name);
|
|
379
|
+
}
|
|
222
380
|
const column = byName.get(key);
|
|
223
381
|
if (!column)
|
|
224
|
-
throw unknownColumn(key, names,
|
|
225
|
-
return
|
|
382
|
+
throw unknownColumn(key, names, view.catalog_name);
|
|
383
|
+
return {
|
|
384
|
+
...column,
|
|
385
|
+
sqlIdent: join ? `${quoteIdent(view.table_name)}.${quoteIdent(column.name)}` : quoteIdent(column.name),
|
|
386
|
+
outputName: column.name,
|
|
387
|
+
joined: false,
|
|
388
|
+
};
|
|
226
389
|
}
|
|
227
390
|
function unknownColumn(name, names, viewName) {
|
|
228
|
-
const
|
|
391
|
+
const needle = name.includes('.') ? name.slice(name.lastIndexOf('.') + 1) : name;
|
|
392
|
+
const suggestions = nearest(needle, names, 3);
|
|
229
393
|
const hint = suggestions.length ? ` Did you mean ${suggestions.join(', ')}?` : '';
|
|
230
394
|
return new ReportViewQueryError('unknown_column', `unknown_column: '${name}' is not a column on ${viewName}.${hint}`);
|
|
231
395
|
}
|
|
@@ -265,8 +429,7 @@ function assertAggregatable(column, fn) {
|
|
|
265
429
|
throw new ReportViewQueryError('aggregate_not_allowed', `aggregate_not_allowed: ${fn} on ${column.name} — did you mean count?`);
|
|
266
430
|
}
|
|
267
431
|
}
|
|
268
|
-
function compileFilter(column, op, value, params, now) {
|
|
269
|
-
const ident = quoteIdent(column.name);
|
|
432
|
+
function compileFilter(column, ident, op, value, params, now, user) {
|
|
270
433
|
if (op === 'is_null')
|
|
271
434
|
return `${ident} IS NULL`;
|
|
272
435
|
if (op === 'not_null')
|
|
@@ -275,7 +438,7 @@ function compileFilter(column, op, value, params, now) {
|
|
|
275
438
|
const list = Array.isArray(value) ? value : null;
|
|
276
439
|
if (!list?.length)
|
|
277
440
|
throw new ReportViewQueryError('value_type_mismatch', `value_type_mismatch: ${op} on ${column.name} needs a non-empty array.`);
|
|
278
|
-
const placeholders = list.map((item) => bind(params, mapEnum(column,
|
|
441
|
+
const placeholders = list.map((item) => bind(params, mapEnum(column, resolveValue(item, column, now, user))));
|
|
279
442
|
return op === 'in' ? `${ident} IN (${placeholders.join(', ')})` : `${ident} NOT IN (${placeholders.join(', ')})`;
|
|
280
443
|
}
|
|
281
444
|
if (op === 'between') {
|
|
@@ -283,35 +446,69 @@ function compileFilter(column, op, value, params, now) {
|
|
|
283
446
|
if (pair?.length !== 2) {
|
|
284
447
|
throw new ReportViewQueryError('value_type_mismatch', `value_type_mismatch: between on ${column.name} needs [from, to].`);
|
|
285
448
|
}
|
|
286
|
-
return `${ident} BETWEEN ${bind(params, mapEnum(column,
|
|
449
|
+
return `${ident} BETWEEN ${bind(params, mapEnum(column, resolveValue(pair[0], column, now, user)))} AND ${bind(params, mapEnum(column, resolveValue(pair[1], column, now, user)))}`;
|
|
287
450
|
}
|
|
288
451
|
if (op === 'contains') {
|
|
289
452
|
if (typeof value !== 'string') {
|
|
290
453
|
throw new ReportViewQueryError('value_type_mismatch', `value_type_mismatch: contains on ${column.name} needs a string.`);
|
|
291
454
|
}
|
|
292
|
-
return `${ident} ILIKE ${bind(params, `%${escapeLike(value)}%`)} ESCAPE '
|
|
455
|
+
return `${ident} ILIKE ${bind(params, `%${escapeLike(value)}%`)} ESCAPE '\\'`;
|
|
293
456
|
}
|
|
294
457
|
if (value === undefined || value === null || Array.isArray(value)) {
|
|
295
458
|
throw new ReportViewQueryError('value_type_mismatch', `value_type_mismatch: ${op} on ${column.name} needs a single value.`);
|
|
296
459
|
}
|
|
297
|
-
const mapped = mapEnum(column,
|
|
460
|
+
const mapped = mapEnum(column, resolveValue(value, column, now, user));
|
|
298
461
|
const sqlOp = { eq: '=', neq: '<>', gt: '>', gte: '>=', lt: '<', lte: '<=' }[op];
|
|
299
462
|
return `${ident} ${sqlOp} ${bind(params, mapped)}`;
|
|
300
463
|
}
|
|
464
|
+
function resolveValue(value, column, now, user) {
|
|
465
|
+
if (value === '@me')
|
|
466
|
+
return resolveMe(column, user);
|
|
467
|
+
return resolveRelativeDate(value, now);
|
|
468
|
+
}
|
|
469
|
+
function resolveMe(column, user) {
|
|
470
|
+
if (!user?.id) {
|
|
471
|
+
throw new ReportViewQueryError('value_type_mismatch', 'value_type_mismatch: @me requires an authenticated caller.');
|
|
472
|
+
}
|
|
473
|
+
const type = column.semantic_type;
|
|
474
|
+
const idLike = type === 'id' || column.name === 'id' || /_id$/.test(column.name);
|
|
475
|
+
if (idLike)
|
|
476
|
+
return user.id;
|
|
477
|
+
if (type === 'money' || type === 'percent' || type === 'date' || type === 'enum') {
|
|
478
|
+
throw new ReportViewQueryError('value_type_mismatch', `value_type_mismatch: @me is not valid on ${column.name} (${type || 'this type'}). Use an id or string column.`);
|
|
479
|
+
}
|
|
480
|
+
if (!user.email) {
|
|
481
|
+
throw new ReportViewQueryError('value_type_mismatch', 'value_type_mismatch: @me on a string column needs the caller email.');
|
|
482
|
+
}
|
|
483
|
+
return user.email;
|
|
484
|
+
}
|
|
301
485
|
function resolveRelativeDate(value, now) {
|
|
302
486
|
if (typeof value !== 'string')
|
|
303
487
|
return value;
|
|
304
488
|
if (value.startsWith('@') && !RELATIVE_DATE.test(value)) {
|
|
305
|
-
throw new ReportViewQueryError('value_type_mismatch', 'value_type_mismatch: relative dates must be @today
|
|
489
|
+
throw new ReportViewQueryError('value_type_mismatch', 'value_type_mismatch: relative dates must be @today, @today±N[d|w|m|y], @month_start, @quarter_start, or @fy_start (e.g. @today-3m, @fy_start). Do not compute today in the client.');
|
|
306
490
|
}
|
|
307
491
|
const match = value.match(RELATIVE_DATE);
|
|
308
492
|
if (!match)
|
|
309
493
|
return value;
|
|
310
494
|
const today = calendarDate(now, RELATIVE_DATE_TZ);
|
|
495
|
+
if (value === '@month_start')
|
|
496
|
+
return monthStart(today);
|
|
497
|
+
if (value === '@quarter_start')
|
|
498
|
+
return quarterStart(today);
|
|
499
|
+
if (value === '@fy_start')
|
|
500
|
+
return fyStart(today);
|
|
311
501
|
if (!match[1])
|
|
312
502
|
return today;
|
|
313
503
|
const delta = Number(match[2]) * (match[1] === '-' ? -1 : 1);
|
|
314
|
-
|
|
504
|
+
const unit = match[3];
|
|
505
|
+
if (unit === 'd')
|
|
506
|
+
return addCalendarDays(today, delta);
|
|
507
|
+
if (unit === 'w')
|
|
508
|
+
return addCalendarDays(today, delta * 7);
|
|
509
|
+
if (unit === 'm')
|
|
510
|
+
return addCalendarMonths(today, delta);
|
|
511
|
+
return addCalendarMonths(today, delta * 12);
|
|
315
512
|
}
|
|
316
513
|
function calendarDate(now, timeZone) {
|
|
317
514
|
return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
@@ -321,6 +518,28 @@ function addCalendarDays(isoDate, days) {
|
|
|
321
518
|
const utc = new Date(Date.UTC(year, month - 1, day + days));
|
|
322
519
|
return utc.toISOString().slice(0, 10);
|
|
323
520
|
}
|
|
521
|
+
function addCalendarMonths(isoDate, months) {
|
|
522
|
+
const [year, month, day] = isoDate.split('-').map(Number);
|
|
523
|
+
const total = year * 12 + (month - 1) + months;
|
|
524
|
+
const nextYear = Math.floor(total / 12);
|
|
525
|
+
const nextMonth = ((total % 12) + 12) % 12;
|
|
526
|
+
const lastDay = new Date(Date.UTC(nextYear, nextMonth + 1, 0)).getUTCDate();
|
|
527
|
+
const clamped = Math.min(day, lastDay);
|
|
528
|
+
return `${nextYear}-${String(nextMonth + 1).padStart(2, '0')}-${String(clamped).padStart(2, '0')}`;
|
|
529
|
+
}
|
|
530
|
+
function monthStart(isoDate) {
|
|
531
|
+
return `${isoDate.slice(0, 7)}-01`;
|
|
532
|
+
}
|
|
533
|
+
function quarterStart(isoDate) {
|
|
534
|
+
const month = Number(isoDate.slice(5, 7));
|
|
535
|
+
const start = Math.floor((month - 1) / 3) * 3 + 1;
|
|
536
|
+
return `${isoDate.slice(0, 4)}-${String(start).padStart(2, '0')}-01`;
|
|
537
|
+
}
|
|
538
|
+
function fyStart(isoDate) {
|
|
539
|
+
const year = Number(isoDate.slice(0, 4));
|
|
540
|
+
const month = Number(isoDate.slice(5, 7));
|
|
541
|
+
return month >= FY_START_MONTH ? `${year}-04-01` : `${year - 1}-04-01`;
|
|
542
|
+
}
|
|
324
543
|
function mapEnum(column, value) {
|
|
325
544
|
const map = column.enum_map;
|
|
326
545
|
if (!map || value === undefined || value === null || typeof value === 'object')
|
|
@@ -344,11 +563,19 @@ function assertInt(value, field) {
|
|
|
344
563
|
throw new ReportViewQueryError('value_type_mismatch', `value_type_mismatch: ${field} must be an integer.`);
|
|
345
564
|
return n;
|
|
346
565
|
}
|
|
347
|
-
function
|
|
348
|
-
const banned = new Set([view.tenancy_column, 'business_id']);
|
|
566
|
+
function assertNoInjectedColumnLeak(dsl, view, names, join) {
|
|
567
|
+
const banned = new Set([view.tenancy_column, 'business_id', view.user_column].filter(Boolean));
|
|
568
|
+
if (join?.target) {
|
|
569
|
+
banned.add(join.target.tenancy_column || 'business_id');
|
|
570
|
+
if (join.target.user_column)
|
|
571
|
+
banned.add(join.target.user_column);
|
|
572
|
+
}
|
|
349
573
|
const check = (value) => {
|
|
350
574
|
const key = String(value || '');
|
|
351
|
-
if (
|
|
575
|
+
if (!key)
|
|
576
|
+
return;
|
|
577
|
+
const column = key.includes('.') ? key.slice(key.lastIndexOf('.') + 1) : key;
|
|
578
|
+
if (banned.has(key) || banned.has(column))
|
|
352
579
|
throw unknownColumn(key, names, view.catalog_name);
|
|
353
580
|
};
|
|
354
581
|
(dsl.columns || []).forEach(check);
|
|
@@ -395,5 +622,6 @@ exports.ReportViewQueryCompiler = {
|
|
|
395
622
|
quoteIdent,
|
|
396
623
|
resolveRelativeDate,
|
|
397
624
|
nearest,
|
|
625
|
+
RELATIVE_DATE_TOKENS: exports.RELATIVE_DATE_TOKENS,
|
|
398
626
|
};
|
|
399
627
|
//# sourceMappingURL=report.view.query.compiler.js.map
|