@oneuptime/common 12.0.1 → 12.0.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/Models/DatabaseModels/Host.ts +12 -4
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +47 -5
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +47 -5
- package/Server/Services/DatabaseService.ts +87 -6
- package/Server/Services/DockerHostService.ts +47 -5
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +48 -5
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +48 -5
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +47 -5
- package/Server/Services/KubernetesClusterService.ts +48 -5
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +106 -1
- package/Server/Services/MonitorService.ts +57 -0
- package/Server/Services/PodmanHostService.ts +47 -5
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +47 -5
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +48 -5
- package/Server/Services/ServerlessFunctionService.ts +48 -5
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Date.ts +112 -0
- package/Types/Events/Recurring.ts +99 -1
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +42 -5
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +42 -5
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +69 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +42 -5
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +43 -5
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +42 -5
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +92 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +47 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +42 -5
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +43 -5
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
2
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
3
|
+
import EndsWith from "../../../Types/BaseDatabase/EndsWith";
|
|
4
|
+
import EqualTo from "../../../Types/BaseDatabase/EqualTo";
|
|
5
|
+
import EqualToOrNull from "../../../Types/BaseDatabase/EqualToOrNull";
|
|
6
|
+
import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
|
|
7
|
+
import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
|
|
8
|
+
import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
|
|
9
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
10
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
11
|
+
import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
|
|
12
|
+
import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
|
|
13
|
+
import IsNull from "../../../Types/BaseDatabase/IsNull";
|
|
14
|
+
import LessThan from "../../../Types/BaseDatabase/LessThan";
|
|
15
|
+
import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
|
|
16
|
+
import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
|
|
17
|
+
import NotContains from "../../../Types/BaseDatabase/NotContains";
|
|
18
|
+
import NotEqual from "../../../Types/BaseDatabase/NotEqual";
|
|
19
|
+
import NotNull from "../../../Types/BaseDatabase/NotNull";
|
|
20
|
+
import Search from "../../../Types/BaseDatabase/Search";
|
|
21
|
+
import StartsWith from "../../../Types/BaseDatabase/StartsWith";
|
|
22
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
23
|
+
import Text from "../../../Types/Text";
|
|
24
|
+
import { JSONObject, JSONValue } from "../../../Types/JSON";
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* ---------------------------------------------------------------------------
|
|
28
|
+
* Querying keys inside a jsonb column
|
|
29
|
+
* ---------------------------------------------------------------------------
|
|
30
|
+
*
|
|
31
|
+
* `customFields` (and every other jsonb column) stores a flat object keyed by
|
|
32
|
+
* the custom field definition's *name*, so "incidents whose Team is Payments"
|
|
33
|
+
* means reaching inside the column for one key.
|
|
34
|
+
*
|
|
35
|
+
* Three properties of this module are load-bearing:
|
|
36
|
+
*
|
|
37
|
+
* 1. THE KEY IS A BOUND PARAMETER, never interpolated. A custom field's name
|
|
38
|
+
* is user-supplied text that arrives on the query from the browser, so
|
|
39
|
+
* splicing it into the SQL string — which is what this code used to do —
|
|
40
|
+
* let anyone who could name a key write SQL into the WHERE clause.
|
|
41
|
+
*
|
|
42
|
+
* 2. A VALUE MATCHES WHETHER IT IS STORED AS A SCALAR OR INSIDE AN ARRAY.
|
|
43
|
+
* A single-select dropdown stores `"Payments"`; the same field switched to
|
|
44
|
+
* multi-select stores `["Payments", "Billing"]`. Both have to answer yes to
|
|
45
|
+
* "Team is Payments", or a project that changed a field's type would find
|
|
46
|
+
* its old rows quietly missing from every filter.
|
|
47
|
+
*
|
|
48
|
+
* 3. A ROW MISSING THE KEY SATISFIES A NEGATIVE FILTER. "Team is not
|
|
49
|
+
* Payments" includes the incidents with no Team at all — which is what the
|
|
50
|
+
* equivalent ClickHouse attribute path already does (StatementGenerator),
|
|
51
|
+
* so the two read the same way in the product.
|
|
52
|
+
*
|
|
53
|
+
* Operators reuse the vocabulary the rest of the query layer already speaks
|
|
54
|
+
* (Search, Includes, NotEqual, GreaterThan, IsNull, ...), so a filter built in
|
|
55
|
+
* the browser serializes and reconstructs with no new wire format.
|
|
56
|
+
*
|
|
57
|
+
* Every predicate is composed from `JsonAccessor`, which is derived from the
|
|
58
|
+
* column reference only at render time — parameters are bound once, up front,
|
|
59
|
+
* so `toSql` can be called more than once without minting new placeholders.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The two ways to read one key out of a jsonb column. `->>` yields text, for
|
|
64
|
+
* comparison; `->` yields jsonb, for array containment and type inspection.
|
|
65
|
+
*/
|
|
66
|
+
interface JsonAccessor {
|
|
67
|
+
/** `col ->> 'key'` — the value as text. NULL when the key is absent. */
|
|
68
|
+
asText: string;
|
|
69
|
+
/** `col -> 'key'` — the value as jsonb. NULL when the key is absent. */
|
|
70
|
+
asJson: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A WHERE fragment that still needs to be told which column it reads. */
|
|
74
|
+
type PredicateFragment = (accessor: JsonAccessor) => string;
|
|
75
|
+
|
|
76
|
+
export interface JSONColumnQuery {
|
|
77
|
+
/**
|
|
78
|
+
* Bound parameters for the emitted SQL. The names are opaque; nothing may
|
|
79
|
+
* depend on them beyond handing them to TypeORM's `Raw`.
|
|
80
|
+
*/
|
|
81
|
+
parameters: Dictionary<JSONValue>;
|
|
82
|
+
/**
|
|
83
|
+
* Render the WHERE fragment for an already-quoted column reference such as
|
|
84
|
+
* `"Incident"."customFields"`.
|
|
85
|
+
*/
|
|
86
|
+
toSql: (columnReference: string) => string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type ParameterNameGeneratorFunction = () => string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Matches an optionally-signed decimal with optional surrounding whitespace.
|
|
93
|
+
* Guards the numeric cast: a jsonb value is arbitrary text, and
|
|
94
|
+
* `CAST('abc' AS NUMERIC)` aborts the whole query rather than skipping a row.
|
|
95
|
+
*
|
|
96
|
+
* This is load-bearing rather than defensive. A Number custom field is stored
|
|
97
|
+
* as whatever the text input produced — `"42"`, not `42` — so every numeric
|
|
98
|
+
* comparison runs through a cast, on rows whose value may be anything at all.
|
|
99
|
+
*/
|
|
100
|
+
const NUMERIC_TEXT_REGEX: string = "^\\s*-?[0-9]+(\\.[0-9]+)?\\s*$";
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* Sanity bounds on an incoming JSON query. Nothing the product builds comes
|
|
104
|
+
* close to these — a project's custom fields are capped well below the key
|
|
105
|
+
* limit and a facet selection below the value limit — so exceeding one means
|
|
106
|
+
* a hand-built request, and answering it with a 400 is better than compiling
|
|
107
|
+
* a megabyte of SQL.
|
|
108
|
+
*/
|
|
109
|
+
export const MAX_JSON_QUERY_KEYS: number = 50;
|
|
110
|
+
export const MAX_JSON_QUERY_VALUES_PER_KEY: number = 200;
|
|
111
|
+
export const MAX_JSON_QUERY_KEY_LENGTH: number = 500;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Escape the characters Postgres' LIKE grammar reserves, so a custom field
|
|
115
|
+
* value containing `%` filters for a literal `%` rather than for anything.
|
|
116
|
+
*
|
|
117
|
+
* `\` is LIKE's default escape character, so it has to be doubled first.
|
|
118
|
+
*/
|
|
119
|
+
export type EscapeLikePatternFunction = (value: string) => string;
|
|
120
|
+
|
|
121
|
+
export const escapeLikePattern: EscapeLikePatternFunction = (
|
|
122
|
+
value: string,
|
|
123
|
+
): string => {
|
|
124
|
+
return value.replace(/([\\%_])/g, "\\$1");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
type IsScalarFunction = (value: unknown) => value is string | number | boolean;
|
|
128
|
+
|
|
129
|
+
const isScalar: IsScalarFunction = (
|
|
130
|
+
value: unknown,
|
|
131
|
+
): value is string | number | boolean => {
|
|
132
|
+
return (
|
|
133
|
+
typeof value === "string" ||
|
|
134
|
+
typeof value === "number" ||
|
|
135
|
+
typeof value === "boolean"
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type ScalarValue = string | number | boolean;
|
|
140
|
+
|
|
141
|
+
type ToScalarFunction = (value: unknown) => ScalarValue;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reduce an operator's payload to something comparable against jsonb.
|
|
145
|
+
* ObjectID and friends stringify; primitives keep their type so the
|
|
146
|
+
* containment literal below is `[42]` rather than `["42"]`.
|
|
147
|
+
*/
|
|
148
|
+
const toScalar: ToScalarFunction = (value: unknown): ScalarValue => {
|
|
149
|
+
if (isScalar(value)) {
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (value instanceof ObjectID) {
|
|
154
|
+
return value.toString();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (value === null || value === undefined) {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return String(value);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type ToScalarArrayFunction = (values: unknown) => Array<ScalarValue>;
|
|
165
|
+
|
|
166
|
+
const toScalarArray: ToScalarArrayFunction = (
|
|
167
|
+
values: unknown,
|
|
168
|
+
): Array<ScalarValue> => {
|
|
169
|
+
if (!Array.isArray(values)) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return values.map((value: unknown) => {
|
|
174
|
+
return toScalar(value);
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type IsNumericValueFunction = (value: unknown) => boolean;
|
|
179
|
+
|
|
180
|
+
const isNumericValue: IsNumericValueFunction = (value: unknown): boolean => {
|
|
181
|
+
if (typeof value === "number") {
|
|
182
|
+
return Number.isFinite(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
186
|
+
return Number.isFinite(Number(value));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Builds one key's predicate. Every helper binds its parameters immediately
|
|
194
|
+
* and returns a fragment that only needs the column reference, so the caller
|
|
195
|
+
* owns a complete parameter bag before any SQL is rendered.
|
|
196
|
+
*/
|
|
197
|
+
class KeyPredicateBuilder {
|
|
198
|
+
private parameters: Dictionary<JSONValue>;
|
|
199
|
+
private generateParameterName: ParameterNameGeneratorFunction;
|
|
200
|
+
private keyParameterName: string;
|
|
201
|
+
|
|
202
|
+
public constructor(data: {
|
|
203
|
+
parameters: Dictionary<JSONValue>;
|
|
204
|
+
generateParameterName: ParameterNameGeneratorFunction;
|
|
205
|
+
key: string;
|
|
206
|
+
}) {
|
|
207
|
+
this.parameters = data.parameters;
|
|
208
|
+
this.generateParameterName = data.generateParameterName;
|
|
209
|
+
this.keyParameterName = this.bind(data.key);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Register a value and return its `:name` placeholder. */
|
|
213
|
+
private bind(value: JSONValue): string {
|
|
214
|
+
const name: string = this.generateParameterName();
|
|
215
|
+
this.parameters[name] = value;
|
|
216
|
+
return `:${name}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/*
|
|
220
|
+
* The key is cast explicitly because `->` / `->>` are overloaded on
|
|
221
|
+
* (jsonb, text) and (jsonb, integer); an untyped bind parameter would leave
|
|
222
|
+
* Postgres unable to choose and fail with "operator is not unique".
|
|
223
|
+
*/
|
|
224
|
+
public accessorFor(columnReference: string): JsonAccessor {
|
|
225
|
+
return {
|
|
226
|
+
asText: `${columnReference} ->> CAST(${this.keyParameterName} AS TEXT)`,
|
|
227
|
+
asJson: `${columnReference} -> CAST(${this.keyParameterName} AS TEXT)`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* "The value at this key is `value`" — true for a scalar that equals it and
|
|
233
|
+
* for an array that contains it.
|
|
234
|
+
*/
|
|
235
|
+
private equals(value: ScalarValue): PredicateFragment {
|
|
236
|
+
const textParameter: string = this.bind(String(value));
|
|
237
|
+
const jsonParameter: string = this.bind(JSON.stringify([value]));
|
|
238
|
+
|
|
239
|
+
return (accessor: JsonAccessor): string => {
|
|
240
|
+
return `(${accessor.asText} = CAST(${textParameter} AS TEXT) OR (jsonb_typeof(${accessor.asJson}) = 'array' AND ${accessor.asJson} @> CAST(${jsonParameter} AS JSONB)))`;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private join(
|
|
245
|
+
fragments: Array<PredicateFragment>,
|
|
246
|
+
operator: "AND" | "OR",
|
|
247
|
+
): PredicateFragment {
|
|
248
|
+
return (accessor: JsonAccessor): string => {
|
|
249
|
+
return `(${fragments
|
|
250
|
+
.map((fragment: PredicateFragment) => {
|
|
251
|
+
return fragment(accessor);
|
|
252
|
+
})
|
|
253
|
+
.join(` ${operator} `)})`;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private anyOf(values: Array<ScalarValue>): PredicateFragment {
|
|
258
|
+
return this.join(
|
|
259
|
+
values.map((value: ScalarValue) => {
|
|
260
|
+
return this.equals(value);
|
|
261
|
+
}),
|
|
262
|
+
"OR",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private allOf(values: Array<ScalarValue>): PredicateFragment {
|
|
267
|
+
return this.join(
|
|
268
|
+
values.map((value: ScalarValue) => {
|
|
269
|
+
return this.equals(value);
|
|
270
|
+
}),
|
|
271
|
+
"AND",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* `pattern` arrives with its wildcards already placed by the caller; the
|
|
277
|
+
* needle inside it has been escaped, so only the caller's own `%` are live.
|
|
278
|
+
*
|
|
279
|
+
* An array-valued key is matched against its JSON text (`["a", "b"]`), which
|
|
280
|
+
* is what makes "contains" work on a multi-select without a second branch.
|
|
281
|
+
*/
|
|
282
|
+
private like(pattern: string): PredicateFragment {
|
|
283
|
+
const parameter: string = this.bind(pattern);
|
|
284
|
+
|
|
285
|
+
return (accessor: JsonAccessor): string => {
|
|
286
|
+
return `(${accessor.asText} ILIKE ${parameter})`;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The value at this key as NUMERIC, or NULL when it does not look like a
|
|
292
|
+
* number. A CASE rather than an AND guard because Postgres does not promise
|
|
293
|
+
* to evaluate the guard first, and one stray cast error aborts the query
|
|
294
|
+
* instead of skipping the row.
|
|
295
|
+
*/
|
|
296
|
+
private numericExpression(accessor: JsonAccessor): string {
|
|
297
|
+
return `(CASE WHEN ${accessor.asText} ~ '${NUMERIC_TEXT_REGEX}' THEN CAST(${accessor.asText} AS NUMERIC) ELSE NULL END)`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private compareNumeric(operator: string, value: unknown): PredicateFragment {
|
|
301
|
+
const parameter: string = this.bind(Number(toScalar(value)));
|
|
302
|
+
|
|
303
|
+
return (accessor: JsonAccessor): string => {
|
|
304
|
+
return `(${this.numericExpression(accessor)} ${operator} CAST(${parameter} AS NUMERIC))`;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private compareText(operator: string, value: unknown): PredicateFragment {
|
|
309
|
+
const parameter: string = this.bind(String(toScalar(value)));
|
|
310
|
+
|
|
311
|
+
return (accessor: JsonAccessor): string => {
|
|
312
|
+
return `(${accessor.asText} ${operator} CAST(${parameter} AS TEXT))`;
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* "No value here" — the key is absent, explicitly JSON null, an empty
|
|
318
|
+
* string, or an empty array. A multi-select cleared in the UI leaves `[]`
|
|
319
|
+
* behind rather than dropping the key, and a user reading "is empty" on the
|
|
320
|
+
* chip means all four.
|
|
321
|
+
*/
|
|
322
|
+
private isEmpty(): PredicateFragment {
|
|
323
|
+
return (accessor: JsonAccessor): string => {
|
|
324
|
+
return `(${accessor.asJson} IS NULL OR jsonb_typeof(${accessor.asJson}) = 'null' OR ${accessor.asText} = '' OR (jsonb_typeof(${accessor.asJson}) = 'array' AND jsonb_array_length(${accessor.asJson}) = 0))`;
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/*
|
|
329
|
+
* `IS NOT TRUE` rather than `NOT (...)`: a row missing the key compares to
|
|
330
|
+
* NULL, and `NOT NULL` is NULL — which would exclude exactly the rows a
|
|
331
|
+
* negative filter is asked about.
|
|
332
|
+
*/
|
|
333
|
+
private negate(fragment: PredicateFragment): PredicateFragment {
|
|
334
|
+
return (accessor: JsonAccessor): string => {
|
|
335
|
+
return `(${fragment(accessor)} IS NOT TRUE)`;
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* A multi-value condition expands to one OR-ed predicate per value, so an
|
|
341
|
+
* unbounded list is an unbounded query. Refuse rather than compile it: the
|
|
342
|
+
* caller gets a 400 they can act on instead of a statement the database
|
|
343
|
+
* spends a minute planning.
|
|
344
|
+
*/
|
|
345
|
+
private assertValueCountWithinLimit(value: unknown): void {
|
|
346
|
+
let count: number = 0;
|
|
347
|
+
|
|
348
|
+
if (Array.isArray(value)) {
|
|
349
|
+
count = value.length;
|
|
350
|
+
} else if (
|
|
351
|
+
value instanceof Includes ||
|
|
352
|
+
value instanceof IncludesAll ||
|
|
353
|
+
value instanceof IncludesNone
|
|
354
|
+
) {
|
|
355
|
+
count = (value.values || []).length;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (count > MAX_JSON_QUERY_VALUES_PER_KEY) {
|
|
359
|
+
throw new BadDataException(
|
|
360
|
+
`A JSON column filter cannot match against more than ${MAX_JSON_QUERY_VALUES_PER_KEY} values at once.`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private containsJson(value: unknown): PredicateFragment {
|
|
366
|
+
const parameter: string = this.bind(JSON.stringify(value));
|
|
367
|
+
|
|
368
|
+
return (accessor: JsonAccessor): string => {
|
|
369
|
+
return `(${accessor.asJson} @> CAST(${parameter} AS JSONB))`;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Build this key's predicate, or null when the condition constrains nothing
|
|
375
|
+
* — an "is any of" with nothing selected. Dropping it is right: narrowing to
|
|
376
|
+
* zero rows would hide the table behind a filter the user just cleared.
|
|
377
|
+
*/
|
|
378
|
+
public build(value: unknown): PredicateFragment | null {
|
|
379
|
+
this.assertValueCountWithinLimit(value);
|
|
380
|
+
|
|
381
|
+
if (value instanceof IsNull) {
|
|
382
|
+
return this.isEmpty();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (value instanceof NotNull) {
|
|
386
|
+
return this.negate(this.isEmpty());
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (value instanceof IncludesAll) {
|
|
390
|
+
const values: Array<ScalarValue> = toScalarArray(value.values);
|
|
391
|
+
return values.length === 0 ? null : this.allOf(values);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (value instanceof IncludesNone) {
|
|
395
|
+
const values: Array<ScalarValue> = toScalarArray(value.values);
|
|
396
|
+
return values.length === 0 ? null : this.negate(this.anyOf(values));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (value instanceof Includes) {
|
|
400
|
+
const values: Array<ScalarValue> = toScalarArray(value.values);
|
|
401
|
+
return values.length === 0 ? null : this.anyOf(values);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (value instanceof NotEqual) {
|
|
405
|
+
return this.negate(this.equals(toScalar(value.value)));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (value instanceof EqualToOrNull) {
|
|
409
|
+
return this.join(
|
|
410
|
+
[this.equals(toScalar(value.value)), this.isEmpty()],
|
|
411
|
+
"OR",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (value instanceof EqualTo) {
|
|
416
|
+
return this.equals(toScalar(value.value));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (value instanceof NotContains) {
|
|
420
|
+
return this.negate(
|
|
421
|
+
this.like(`%${escapeLikePattern(String(toScalar(value.value)))}%`),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (value instanceof StartsWith) {
|
|
426
|
+
return this.like(`${escapeLikePattern(String(toScalar(value.value)))}%`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (value instanceof EndsWith) {
|
|
430
|
+
return this.like(`%${escapeLikePattern(String(toScalar(value.value)))}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (value instanceof Search) {
|
|
434
|
+
return this.like(`%${escapeLikePattern(String(toScalar(value.value)))}%`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (value instanceof InBetween) {
|
|
438
|
+
const start: unknown = value.startValue;
|
|
439
|
+
const end: unknown = value.endValue;
|
|
440
|
+
|
|
441
|
+
if (isNumericValue(start) && isNumericValue(end)) {
|
|
442
|
+
return this.join(
|
|
443
|
+
[this.compareNumeric(">=", start), this.compareNumeric("<=", end)],
|
|
444
|
+
"AND",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return this.join(
|
|
449
|
+
[this.compareText(">=", start), this.compareText("<=", end)],
|
|
450
|
+
"AND",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (value instanceof GreaterThanOrNull) {
|
|
455
|
+
return this.join(
|
|
456
|
+
[this.compareNumeric(">=", value.value), this.isEmpty()],
|
|
457
|
+
"OR",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (value instanceof LessThanOrNull) {
|
|
462
|
+
return this.join(
|
|
463
|
+
[this.compareNumeric("<=", value.value), this.isEmpty()],
|
|
464
|
+
"OR",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (value instanceof GreaterThanOrEqual) {
|
|
469
|
+
return this.compareNumeric(">=", value.value);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (value instanceof GreaterThan) {
|
|
473
|
+
return this.compareNumeric(">", value.value);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (value instanceof LessThanOrEqual) {
|
|
477
|
+
return this.compareNumeric("<=", value.value);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (value instanceof LessThan) {
|
|
481
|
+
return this.compareNumeric("<", value.value);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/*
|
|
485
|
+
* A bare array reads as "is any of" — the same meaning QueryUtil gives a
|
|
486
|
+
* top-level array, which it wraps in an ANY.
|
|
487
|
+
*/
|
|
488
|
+
if (Array.isArray(value)) {
|
|
489
|
+
const values: Array<ScalarValue> = toScalarArray(value);
|
|
490
|
+
return values.length === 0 ? null : this.anyOf(values);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (isScalar(value)) {
|
|
494
|
+
return this.equals(value);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (value === null || value === undefined) {
|
|
498
|
+
return this.isEmpty();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/*
|
|
502
|
+
* Anything left is a nested object. Containment is the only sensible
|
|
503
|
+
* reading, and a strict improvement on what this used to do — compare the
|
|
504
|
+
* column's text against "[object Object]".
|
|
505
|
+
*/
|
|
506
|
+
return this.containsJson(value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export type BuildJSONColumnQueryFunction = (data: {
|
|
511
|
+
value: JSONObject | string;
|
|
512
|
+
generateParameterName?: ParameterNameGeneratorFunction | undefined;
|
|
513
|
+
}) => JSONColumnQuery;
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Turn `{ Team: Includes(["Payments"]), Region: "us-east-1" }` into one
|
|
517
|
+
* AND-joined WHERE fragment over a jsonb column, plus the parameters it binds.
|
|
518
|
+
*
|
|
519
|
+
* An empty object keeps its historical meaning — "this column holds nothing" —
|
|
520
|
+
* because that is what a JSON filter with every row cleared has always sent.
|
|
521
|
+
*/
|
|
522
|
+
const buildJSONColumnQuery: BuildJSONColumnQueryFunction = (data: {
|
|
523
|
+
value: JSONObject | string;
|
|
524
|
+
generateParameterName?: ParameterNameGeneratorFunction | undefined;
|
|
525
|
+
}): JSONColumnQuery => {
|
|
526
|
+
const generateParameterName: ParameterNameGeneratorFunction =
|
|
527
|
+
data.generateParameterName ||
|
|
528
|
+
((): string => {
|
|
529
|
+
return Text.generateRandomText(10);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
let value: JSONObject = {};
|
|
533
|
+
|
|
534
|
+
if (typeof data.value === "string") {
|
|
535
|
+
try {
|
|
536
|
+
value = JSON.parse(data.value) as JSONObject;
|
|
537
|
+
} catch {
|
|
538
|
+
/*
|
|
539
|
+
* A JSON column query that is not parseable JSON cannot be turned into a
|
|
540
|
+
* predicate. Falling back to the empty object below means "this column
|
|
541
|
+
* holds nothing", which is at least a filter the caller can see the
|
|
542
|
+
* effect of — as opposed to a 500.
|
|
543
|
+
*/
|
|
544
|
+
value = {};
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
value = data.value || {};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const parameters: Dictionary<JSONValue> = {};
|
|
551
|
+
const fragments: Array<(columnReference: string) => string> = [];
|
|
552
|
+
|
|
553
|
+
const keys: Array<string> = Object.keys(value || {});
|
|
554
|
+
|
|
555
|
+
if (keys.length > MAX_JSON_QUERY_KEYS) {
|
|
556
|
+
throw new BadDataException(
|
|
557
|
+
`A JSON column filter cannot carry more than ${MAX_JSON_QUERY_KEYS} keys.`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (const key of keys) {
|
|
562
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (key.length > MAX_JSON_QUERY_KEY_LENGTH) {
|
|
567
|
+
throw new BadDataException(
|
|
568
|
+
`A JSON column filter key cannot be longer than ${MAX_JSON_QUERY_KEY_LENGTH} characters.`,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const builder: KeyPredicateBuilder = new KeyPredicateBuilder({
|
|
573
|
+
parameters: parameters,
|
|
574
|
+
generateParameterName: generateParameterName,
|
|
575
|
+
key: key,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const fragment: PredicateFragment | null = builder.build(
|
|
579
|
+
(value as JSONObject)[key],
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
if (!fragment) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
fragments.push((columnReference: string): string => {
|
|
587
|
+
return fragment(builder.accessorFor(columnReference));
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
parameters: parameters,
|
|
593
|
+
toSql: (columnReference: string): string => {
|
|
594
|
+
if (keys.length === 0) {
|
|
595
|
+
/*
|
|
596
|
+
* Historical meaning of `{}`, kept verbatim: the column itself is
|
|
597
|
+
* empty. Changing it would silently retarget every saved filter that
|
|
598
|
+
* has ever sent an empty JSON filter.
|
|
599
|
+
*/
|
|
600
|
+
return `(${columnReference} IS NULL OR ${columnReference} = '{}')`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (fragments.length === 0) {
|
|
604
|
+
/*
|
|
605
|
+
* Every condition turned out to constrain nothing. Matching everything
|
|
606
|
+
* is right; matching nothing would hide the whole table behind a
|
|
607
|
+
* filter the user had cleared.
|
|
608
|
+
*/
|
|
609
|
+
return "(1 = 1)";
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return `(${fragments
|
|
613
|
+
.map((fragment: (columnReference: string) => string) => {
|
|
614
|
+
return fragment(columnReference);
|
|
615
|
+
})
|
|
616
|
+
.join(" AND ")})`;
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
export default buildJSONColumnQuery;
|
|
@@ -35,9 +35,16 @@ export default class PublicPermission {
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/*
|
|
39
|
+
* The record is not publicly accessible and the caller presented
|
|
40
|
+
* neither a session nor an API key. Name both, the way the analytics
|
|
41
|
+
* counterpart of this check already does — the old wording talked only
|
|
42
|
+
* about logging in, which reads as an RBAC failure on the named model
|
|
43
|
+
* to anyone authenticating by key, and repeatedly sent API callers off
|
|
44
|
+
* to audit permissions when their key had simply not arrived.
|
|
45
|
+
*/
|
|
39
46
|
throw new NotAuthenticatedException(
|
|
40
|
-
`
|
|
47
|
+
`Authenticated user or a valid API key is needed to ${type} record of ${
|
|
41
48
|
new modelType().singularName
|
|
42
49
|
}.`,
|
|
43
50
|
);
|