@segmentstream-cloud/ontology 0.15.0 → 0.17.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/README.md +49 -1
- package/dist/aggregate-filters.d.ts +48 -0
- package/dist/aggregate-filters.js +252 -0
- package/dist/authoring-records.d.ts +5 -0
- package/dist/authoring-records.js +5 -1
- package/dist/authoring.js +32 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +109 -60
- package/dist/manifest.js +43 -27
- package/dist/measure.d.ts +86 -7
- package/dist/measure.js +92 -15
- package/dist/migrate/apply.js +2 -0
- package/dist/migrate/model.d.ts +12 -8
- package/dist/migrate/plan.js +14 -4
- package/dist/render.d.ts +6 -3
- package/dist/render.js +158 -2
- package/dist/runtime/service-binding.js +3 -0
- package/dist/service/agent/attachments.d.ts +37 -0
- package/dist/service/agent/attachments.js +225 -0
- package/dist/service/mcp/server.js +28 -5
- package/dist/service/mcp/tools.d.ts +6 -0
- package/dist/service/mcp/tools.js +79 -12
- package/dist/service/runtime-contract.d.ts +3 -0
- package/dist/service/schemas.d.ts +40 -22
- package/dist/service/schemas.js +46 -43
- package/dist/service/service.js +11 -1
- package/dist/storage/attachments.d.ts +117 -0
- package/dist/storage/attachments.js +536 -0
- package/dist/storage/maintenance.d.ts +4 -1
- package/dist/storage/maintenance.js +7 -2
- package/dist/storage/neon.js +14 -0
- package/dist/storage/provider.d.ts +2 -0
- package/dist/storage/tables.d.ts +8 -0
- package/dist/storage/tables.js +32 -0
- package/dist/warehouse-backed.js +199 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ action and is kept in history.
|
|
|
11
11
|
TypeScript definitions, handler records and SDK calls use camelCase: `jobTitle`,
|
|
12
12
|
`assignedOwnerId`, `createdAt`, and `changeStatus`. The SDK converts ontology names,
|
|
13
13
|
select identifiers and declared action input fields to snake_case for the generated
|
|
14
|
-
REST API, version
|
|
14
|
+
REST API, version 7 manifest and storage. TypeScript helpers and options stay
|
|
15
15
|
camelCase. JSON property contents, provider payloads, opaque action results and
|
|
16
16
|
historical change names remain data and are preserved. `formatManifest` serializes
|
|
17
17
|
the canonical wire contract shared by REST, storage and future transports.
|
|
@@ -28,6 +28,15 @@ one Effect version.
|
|
|
28
28
|
`ontology/` is a package of its own. The SDK is an ES module, so `ontology/package.json`
|
|
29
29
|
must set `"type": "module"`.
|
|
30
30
|
|
|
31
|
+
SDK 0.17.0 writes manifest version 7. It adds `m.avg`, `m.min` and `m.max`; a `where` option on
|
|
32
|
+
every aggregate, so that a measure reads only the rows its conditions accept (`m.sum("amount",
|
|
33
|
+
{ where: [{ property: "status", operator: "equals", value: "won" }], description })`); and
|
|
34
|
+
`balanceOver`, which names the date a measure's rows are snapshots over, so that requests group by
|
|
35
|
+
it or filter it to one day. Aggregate requests
|
|
36
|
+
take the `notEquals`, `notIn`, `gt`, `gte`, `lt`, `lte`, `contains`, `startsWith`, `endsWith` and
|
|
37
|
+
`inPeriod` operators, the `hour`, `quarter` and `year` granularities and a `timezone`. See
|
|
38
|
+
[ontology aggregates](https://github.com/segmentstream/segmentstream/blob/main/docs/ontology-aggregates.md).
|
|
39
|
+
|
|
31
40
|
SDK 0.14.0 lets the app's own agent call the service as a paired member (see
|
|
32
41
|
[Agents](#agents)). Its migration adds the agent's runtime tables.
|
|
33
42
|
|
|
@@ -311,6 +320,45 @@ pair, or when the agent refuses, the action is rejected with the reason. Both ac
|
|
|
311
320
|
`agent_chat:send_message`), which no role holds until the workspace grants it, and reading
|
|
312
321
|
chats needs `agent_chat:read` by name: `*:read` and `*:*` do not cover `agent_chat`.
|
|
313
322
|
|
|
323
|
+
### Agent attachments
|
|
324
|
+
|
|
325
|
+
A file a member sends the agent is a temporary attachment: platform metadata in the runtime
|
|
326
|
+
table `_agent_attachments`, never an ontology record, with its bytes stored once as a revision in
|
|
327
|
+
the ontology's file storage, beside uploads. It belongs to the member, the chat it arrived in and
|
|
328
|
+
the agent session that first reads it, holds its SHA-256, size, type and name, and expires 24
|
|
329
|
+
hours after receipt (`received` → `ready` → `promoted`, `discarded` or `expired`). Files are up
|
|
330
|
+
to 20 MB, what Telegram lets a bot download; a member holds at most 50 live attachments. The
|
|
331
|
+
table exists on deployments with file storage; the migration adds it beside the file tables, also
|
|
332
|
+
to a database an earlier SDK migrated.
|
|
333
|
+
|
|
334
|
+
Only the agent calls these routes, with its runtime token, `X-SegmentStream-Act-As`, the chat in
|
|
335
|
+
`X-SegmentStream-Agent-Conversation` (`telegram:<chat id>`) and, inside a turn, the session in
|
|
336
|
+
`X-SegmentStream-Agent-Session`. Another member's attachment, another chat's or another
|
|
337
|
+
session's answers 404; an expired or discarded one answers 409; a deactivated member's 403.
|
|
338
|
+
|
|
339
|
+
| Route | Does |
|
|
340
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
341
|
+
| `POST /api/ontology/agent/attachments` | `{ source, source_ref, filename, content_type, size, sha256 }` → `{ attachment, upload? }`, a PUT grant |
|
|
342
|
+
| `POST /api/ontology/agent/attachments/{id}/complete` | Verifies the stored bytes' size and SHA-256 → `{ attachment }` in `ready` |
|
|
343
|
+
| `GET /api/ontology/agent/attachments` | `{ attachments }`: the caller's readable attachments in this chat and session |
|
|
344
|
+
| `GET /api/ontology/agent/attachments/{id}` | `{ attachment, download }`: a one-minute GET grant |
|
|
345
|
+
| `POST /api/ontology/agent/attachments/{id}/promote` | `{ entity, action, input?, field?, record_id? }` → `{ attachment, deduplicated, result }` |
|
|
346
|
+
| `POST /api/ontology/agent/attachments/{id}/discard` | Revokes it at once → `{ attachment }` |
|
|
347
|
+
|
|
348
|
+
Promotion keeps a file by running an action that takes an upload, such as an authored
|
|
349
|
+
`document.upload` with `uploads.for(Document, "content")`, as the member and with their
|
|
350
|
+
permissions. The service mints a completed upload ticket on the attachment's existing revision,
|
|
351
|
+
fills the action's upload field (and `sha256`, when the input has one and the caller left it
|
|
352
|
+
out), and runs the action with an idempotency key derived from the attachment, action and
|
|
353
|
+
input, and the ticket's create intent; the action consumes the ticket as it would a browser
|
|
354
|
+
upload, without copying the bytes. The target property's `accept` and `maxBytes` apply. A file
|
|
355
|
+
whose SHA-256 an earlier attachment already promoted through the same action answers that
|
|
356
|
+
promotion with `deduplicated: true` instead of a second record. Storage maintenance expires
|
|
357
|
+
attachments past their deadline and removes the files no record adopted.
|
|
358
|
+
|
|
359
|
+
An MCP action tool whose input takes an upload also accepts `_options: { idempotency_key,
|
|
360
|
+
create_intent }`, the options the REST route takes beside `input`, so an MCP client can run it.
|
|
361
|
+
|
|
314
362
|
## The `segmentstream-ontology` binary
|
|
315
363
|
|
|
316
364
|
Run the binary in the `ontology/` folder. The entry defaults to `./index.ts`. The binary
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filter operators of aggregates and the values each takes, shared by the service's request
|
|
3
|
+
* schemas and `check`'s measure conditions (docs/ontology-aggregates-release-2-spec.md, "Filters").
|
|
4
|
+
* The rules and messages are SegmentStream's own (backend-next's `semantics/core/predicates.ts`), so
|
|
5
|
+
* the SDK, the service and the platform refuse alike.
|
|
6
|
+
*/
|
|
7
|
+
/** A filter value: a literal. */
|
|
8
|
+
export type AggregateValue = string | number | boolean;
|
|
9
|
+
/** The operators of a model-level aggregate's filters. */
|
|
10
|
+
export declare const AGGREGATE_FILTER_OPERATORS: readonly ["equals", "in", "isNull", "isNotNull", "between", "notEquals", "notIn", "gt", "gte", "lt", "lte", "contains", "startsWith", "endsWith", "inPeriod"];
|
|
11
|
+
/**
|
|
12
|
+
* The operators of the per-entity shorthand, which bounds dates with `dateRange` instead of
|
|
13
|
+
* `between` and `inPeriod`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ENTITY_AGGREGATE_FILTER_OPERATORS: readonly ["equals", "in", "isNull", "isNotNull", "notEquals", "notIn", "gt", "gte", "lt", "lte", "contains", "startsWith", "endsWith"];
|
|
16
|
+
/**
|
|
17
|
+
* The operators of a measure condition: a model-level filter's, except `inPeriod`, whose dates would
|
|
18
|
+
* move with the day a request runs.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MEASURE_CONDITION_OPERATORS: readonly ["equals", "in", "notEquals", "notIn", "isNull", "isNotNull", "between", "gt", "gte", "lt", "lte", "contains", "startsWith", "endsWith"];
|
|
21
|
+
export type MeasureConditionOperator = (typeof MEASURE_CONDITION_OPERATORS)[number];
|
|
22
|
+
/** The periods `inPeriod` takes without a count. Weeks start on Monday. */
|
|
23
|
+
export declare const AGGREGATE_PERIODS: readonly ["today", "yesterday", "this week", "this month", "this quarter", "this year", "this week to date", "this month to date", "this quarter to date", "this year to date", "last week", "last month", "last quarter", "last year"];
|
|
24
|
+
/**
|
|
25
|
+
* The units of `last N <unit>`, N at least 1: `last N days` ends yesterday; the others are the N
|
|
26
|
+
* whole units before the current one.
|
|
27
|
+
*/
|
|
28
|
+
export declare const AGGREGATE_PERIOD_UNITS: readonly ["days", "weeks", "months", "quarters", "years"];
|
|
29
|
+
/** The most values `in` and `notIn` take. */
|
|
30
|
+
export declare const MAXIMUM_FILTER_VALUES = 100;
|
|
31
|
+
/** An operator and the values given to it, before they are known to suit each other. */
|
|
32
|
+
export interface FilterValues {
|
|
33
|
+
readonly operator: string;
|
|
34
|
+
readonly value?: unknown;
|
|
35
|
+
readonly values?: ReadonlyArray<unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** Why an operator does not take the values given, or `undefined` when it does. */
|
|
38
|
+
export declare function filterValuesIssue(filter: FilterValues): string | undefined;
|
|
39
|
+
/** How SegmentStream compares a dimension's values. */
|
|
40
|
+
export type DimensionType = "string" | "decimal" | "boolean" | "date" | "timestamp";
|
|
41
|
+
/** The dimension type of a property type, or `undefined` for JSON and files, which are none. */
|
|
42
|
+
export declare function dimensionTypeOf(propertyType: string): DimensionType | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Why a filter's values do not suit its dimension's type, or `undefined` when they do: the operator
|
|
45
|
+
* must apply to the type and every value must match it. Their count is {@link filterValuesIssue}'s
|
|
46
|
+
* to check first. `inPeriod` values are periods, which SegmentStream resolves.
|
|
47
|
+
*/
|
|
48
|
+
export declare function filterTypeIssue(filter: FilterValues, type: DimensionType): string | undefined;
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filter operators of aggregates and the values each takes, shared by the service's request
|
|
3
|
+
* schemas and `check`'s measure conditions (docs/ontology-aggregates-release-2-spec.md, "Filters").
|
|
4
|
+
* The rules and messages are SegmentStream's own (backend-next's `semantics/core/predicates.ts`), so
|
|
5
|
+
* the SDK, the service and the platform refuse alike.
|
|
6
|
+
*/
|
|
7
|
+
/** The operators of a model-level aggregate's filters. */
|
|
8
|
+
export const AGGREGATE_FILTER_OPERATORS = [
|
|
9
|
+
"equals",
|
|
10
|
+
"in",
|
|
11
|
+
"isNull",
|
|
12
|
+
"isNotNull",
|
|
13
|
+
"between",
|
|
14
|
+
"notEquals",
|
|
15
|
+
"notIn",
|
|
16
|
+
"gt",
|
|
17
|
+
"gte",
|
|
18
|
+
"lt",
|
|
19
|
+
"lte",
|
|
20
|
+
"contains",
|
|
21
|
+
"startsWith",
|
|
22
|
+
"endsWith",
|
|
23
|
+
"inPeriod",
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* The operators of the per-entity shorthand, which bounds dates with `dateRange` instead of
|
|
27
|
+
* `between` and `inPeriod`.
|
|
28
|
+
*/
|
|
29
|
+
export const ENTITY_AGGREGATE_FILTER_OPERATORS = [
|
|
30
|
+
"equals",
|
|
31
|
+
"in",
|
|
32
|
+
"isNull",
|
|
33
|
+
"isNotNull",
|
|
34
|
+
"notEquals",
|
|
35
|
+
"notIn",
|
|
36
|
+
"gt",
|
|
37
|
+
"gte",
|
|
38
|
+
"lt",
|
|
39
|
+
"lte",
|
|
40
|
+
"contains",
|
|
41
|
+
"startsWith",
|
|
42
|
+
"endsWith",
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* The operators of a measure condition: a model-level filter's, except `inPeriod`, whose dates would
|
|
46
|
+
* move with the day a request runs.
|
|
47
|
+
*/
|
|
48
|
+
export const MEASURE_CONDITION_OPERATORS = [
|
|
49
|
+
"equals",
|
|
50
|
+
"in",
|
|
51
|
+
"notEquals",
|
|
52
|
+
"notIn",
|
|
53
|
+
"isNull",
|
|
54
|
+
"isNotNull",
|
|
55
|
+
"between",
|
|
56
|
+
"gt",
|
|
57
|
+
"gte",
|
|
58
|
+
"lt",
|
|
59
|
+
"lte",
|
|
60
|
+
"contains",
|
|
61
|
+
"startsWith",
|
|
62
|
+
"endsWith",
|
|
63
|
+
];
|
|
64
|
+
/** The periods `inPeriod` takes without a count. Weeks start on Monday. */
|
|
65
|
+
export const AGGREGATE_PERIODS = [
|
|
66
|
+
"today",
|
|
67
|
+
"yesterday",
|
|
68
|
+
"this week",
|
|
69
|
+
"this month",
|
|
70
|
+
"this quarter",
|
|
71
|
+
"this year",
|
|
72
|
+
"this week to date",
|
|
73
|
+
"this month to date",
|
|
74
|
+
"this quarter to date",
|
|
75
|
+
"this year to date",
|
|
76
|
+
"last week",
|
|
77
|
+
"last month",
|
|
78
|
+
"last quarter",
|
|
79
|
+
"last year",
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* The units of `last N <unit>`, N at least 1: `last N days` ends yesterday; the others are the N
|
|
83
|
+
* whole units before the current one.
|
|
84
|
+
*/
|
|
85
|
+
export const AGGREGATE_PERIOD_UNITS = ["days", "weeks", "months", "quarters", "years"];
|
|
86
|
+
/** The most values `in` and `notIn` take. */
|
|
87
|
+
export const MAXIMUM_FILTER_VALUES = 100;
|
|
88
|
+
const COMPARISONS = new Set(["gt", "gte", "lt", "lte"]);
|
|
89
|
+
const TEXT_OPERATORS = new Set(["contains", "startsWith", "endsWith"]);
|
|
90
|
+
function oneValue({ value, values }) {
|
|
91
|
+
return value !== undefined && values === undefined;
|
|
92
|
+
}
|
|
93
|
+
function someValues({ value, values }) {
|
|
94
|
+
return (value === undefined &&
|
|
95
|
+
values !== undefined &&
|
|
96
|
+
values.length > 0 &&
|
|
97
|
+
values.length <= MAXIMUM_FILTER_VALUES);
|
|
98
|
+
}
|
|
99
|
+
/** Why an operator does not take the values given, or `undefined` when it does. */
|
|
100
|
+
export function filterValuesIssue(filter) {
|
|
101
|
+
const { operator, value, values } = filter;
|
|
102
|
+
if (COMPARISONS.has(operator)) {
|
|
103
|
+
return oneValue(filter) ? undefined : "Comparison filters require one value.";
|
|
104
|
+
}
|
|
105
|
+
if (TEXT_OPERATORS.has(operator)) {
|
|
106
|
+
return oneValue(filter) && typeof value === "string" && value !== ""
|
|
107
|
+
? undefined
|
|
108
|
+
: "Text filters require one non-empty string.";
|
|
109
|
+
}
|
|
110
|
+
switch (operator) {
|
|
111
|
+
case "equals":
|
|
112
|
+
case "notEquals":
|
|
113
|
+
return oneValue(filter) ? undefined : "Equality filters require one value.";
|
|
114
|
+
case "in":
|
|
115
|
+
return someValues(filter)
|
|
116
|
+
? undefined
|
|
117
|
+
: "Membership filters require between 1 and 100 values.";
|
|
118
|
+
case "notIn":
|
|
119
|
+
return someValues(filter) ? undefined : "notIn requires between 1 and 100 values.";
|
|
120
|
+
case "between":
|
|
121
|
+
return value === undefined && values?.length === 2
|
|
122
|
+
? undefined
|
|
123
|
+
: "between needs two values in order.";
|
|
124
|
+
case "inPeriod":
|
|
125
|
+
// Whether the value is a period is SegmentStream's to say: it resolves it.
|
|
126
|
+
return oneValue(filter) ? undefined : "inPeriod filters require one period.";
|
|
127
|
+
case "isNull":
|
|
128
|
+
case "isNotNull":
|
|
129
|
+
return value === undefined && values === undefined
|
|
130
|
+
? undefined
|
|
131
|
+
: "Null filters do not accept values.";
|
|
132
|
+
default:
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const DIMENSION_TYPES = {
|
|
137
|
+
text: "string",
|
|
138
|
+
email: "string",
|
|
139
|
+
url: "string",
|
|
140
|
+
select: "string",
|
|
141
|
+
integer: "decimal",
|
|
142
|
+
number: "decimal",
|
|
143
|
+
boolean: "boolean",
|
|
144
|
+
date: "date",
|
|
145
|
+
timestamp: "timestamp",
|
|
146
|
+
};
|
|
147
|
+
/** The dimension type of a property type, or `undefined` for JSON and files, which are none. */
|
|
148
|
+
export function dimensionTypeOf(propertyType) {
|
|
149
|
+
return Object.hasOwn(DIMENSION_TYPES, propertyType) ? DIMENSION_TYPES[propertyType] : undefined;
|
|
150
|
+
}
|
|
151
|
+
function isDate(value) {
|
|
152
|
+
return (typeof value === "string" &&
|
|
153
|
+
/^\d{4}-\d{2}-\d{2}$/.test(value) &&
|
|
154
|
+
!Number.isNaN(Date.parse(value)) &&
|
|
155
|
+
new Date(value).toISOString().slice(0, 10) === value);
|
|
156
|
+
}
|
|
157
|
+
/** Whether a value is a literal of the type, as SegmentStream reads filter values. */
|
|
158
|
+
function matches(value, type) {
|
|
159
|
+
if (type === "boolean") {
|
|
160
|
+
return typeof value === "boolean";
|
|
161
|
+
}
|
|
162
|
+
if (type === "decimal") {
|
|
163
|
+
return ((typeof value === "number" || typeof value === "string") &&
|
|
164
|
+
String(value).length <= 100 &&
|
|
165
|
+
/^[-+]?(?:\d+\.?\d*|\.\d+)$/.test(String(value)) &&
|
|
166
|
+
Number.isFinite(Number(value)));
|
|
167
|
+
}
|
|
168
|
+
if (typeof value !== "string" || value.length > 2048) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (type === "timestamp") {
|
|
172
|
+
return (/^\d{4}-\d{2}-\d{2}T/.test(value) &&
|
|
173
|
+
/(?:Z|[+-]\d{2}:\d{2})$/.test(value) &&
|
|
174
|
+
Number.isFinite(Date.parse(value)));
|
|
175
|
+
}
|
|
176
|
+
return type !== "date" || isDate(value);
|
|
177
|
+
}
|
|
178
|
+
/** Bounds of a timestamp may be dates, which cover whole days; then they are compared as dates. */
|
|
179
|
+
function boundType(bounds, type) {
|
|
180
|
+
return type === "timestamp" && bounds.every(isDate) ? "date" : type;
|
|
181
|
+
}
|
|
182
|
+
/** Compares two decimal literals exactly, as their digits say, however many there are. */
|
|
183
|
+
function compareDecimals(left, right) {
|
|
184
|
+
const parse = (value) => {
|
|
185
|
+
const negative = value.startsWith("-");
|
|
186
|
+
const [whole = "", fraction = ""] = value.replace(/^[-+]/, "").split(".");
|
|
187
|
+
const digits = whole.replace(/^0+/, "");
|
|
188
|
+
const decimals = fraction.replace(/0+$/, "");
|
|
189
|
+
const zero = digits === "" && decimals === "";
|
|
190
|
+
return { negative: negative && !zero, digits, decimals };
|
|
191
|
+
};
|
|
192
|
+
const a = parse(left);
|
|
193
|
+
const b = parse(right);
|
|
194
|
+
if (a.negative !== b.negative) {
|
|
195
|
+
return a.negative ? -1 : 1;
|
|
196
|
+
}
|
|
197
|
+
const width = Math.max(a.decimals.length, b.decimals.length);
|
|
198
|
+
const magnitude = (value) => value.digits.padStart(Math.max(a.digits.length, b.digits.length), "0") +
|
|
199
|
+
value.decimals.padEnd(width, "0");
|
|
200
|
+
// Equal-length digit strings order as their values do.
|
|
201
|
+
const [first, second] = [magnitude(a), magnitude(b)];
|
|
202
|
+
if (first === second) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
const sign = first < second ? -1 : 1;
|
|
206
|
+
return a.negative ? -sign : sign;
|
|
207
|
+
}
|
|
208
|
+
function inOrder(from, to, type) {
|
|
209
|
+
if (type === "decimal") {
|
|
210
|
+
return compareDecimals(String(from), String(to)) <= 0;
|
|
211
|
+
}
|
|
212
|
+
if (type === "timestamp") {
|
|
213
|
+
return Date.parse(String(from)) <= Date.parse(String(to));
|
|
214
|
+
}
|
|
215
|
+
return String(from) <= String(to);
|
|
216
|
+
}
|
|
217
|
+
const MISMATCH = "Filter value does not match its dimension type.";
|
|
218
|
+
const ORDERED_TYPES = new Set(["date", "timestamp", "decimal"]);
|
|
219
|
+
/**
|
|
220
|
+
* Why a filter's values do not suit its dimension's type, or `undefined` when they do: the operator
|
|
221
|
+
* must apply to the type and every value must match it. Their count is {@link filterValuesIssue}'s
|
|
222
|
+
* to check first. `inPeriod` values are periods, which SegmentStream resolves.
|
|
223
|
+
*/
|
|
224
|
+
export function filterTypeIssue(filter, type) {
|
|
225
|
+
const { operator, value, values = [] } = filter;
|
|
226
|
+
const given = value === undefined ? values : [value];
|
|
227
|
+
if (operator === "between") {
|
|
228
|
+
const [from, to] = given;
|
|
229
|
+
const bounds = boundType([from, to], type);
|
|
230
|
+
if (!ORDERED_TYPES.has(type)) {
|
|
231
|
+
return "between filters date, timestamp and number dimensions.";
|
|
232
|
+
}
|
|
233
|
+
if (!matches(from, bounds) || !matches(to, bounds)) {
|
|
234
|
+
return MISMATCH;
|
|
235
|
+
}
|
|
236
|
+
return inOrder(from, to, bounds) ? undefined : "between needs two values in order.";
|
|
237
|
+
}
|
|
238
|
+
if (operator === "inPeriod") {
|
|
239
|
+
return type === "date" || type === "timestamp"
|
|
240
|
+
? undefined
|
|
241
|
+
: "inPeriod filters date and timestamp dimensions.";
|
|
242
|
+
}
|
|
243
|
+
if (COMPARISONS.has(operator) && !ORDERED_TYPES.has(type)) {
|
|
244
|
+
return `${operator} filters date, timestamp and number dimensions.`;
|
|
245
|
+
}
|
|
246
|
+
if (TEXT_OPERATORS.has(operator) && type !== "string") {
|
|
247
|
+
return `${operator} filters text dimensions.`;
|
|
248
|
+
}
|
|
249
|
+
// A comparison bounds a timestamp by a date or by an exact instant, as `between` does.
|
|
250
|
+
const valueType = COMPARISONS.has(operator) ? boundType(given, type) : type;
|
|
251
|
+
return given.every((candidate) => matches(candidate, valueType)) ? undefined : MISMATCH;
|
|
252
|
+
}
|
|
@@ -5,6 +5,11 @@ interface Definition {
|
|
|
5
5
|
readonly properties: Properties;
|
|
6
6
|
readonly relationships?: Relationships;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* A select property's values as stored, the option values' wire names, or as authored when
|
|
10
|
+
* `decode`. Other properties' values are data and stay as they are.
|
|
11
|
+
*/
|
|
12
|
+
export declare function selectValue(value: unknown, property: unknown, decode: boolean): unknown;
|
|
8
13
|
/** Only record keys and select identifiers convert; property values remain data. */
|
|
9
14
|
export declare function recordFromWire<A>(value: unknown, definition?: Definition): A;
|
|
10
15
|
export declare function valuesToWire(values: StoredValues, definition: Definition): StoredValues;
|
|
@@ -6,7 +6,11 @@ function targetDefinition(target) {
|
|
|
6
6
|
const resolved = typeof target === "function" ? target() : target;
|
|
7
7
|
return isEntity(resolved) ? resolved.definition : undefined;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A select property's values as stored, the option values' wire names, or as authored when
|
|
11
|
+
* `decode`. Other properties' values are data and stay as they are.
|
|
12
|
+
*/
|
|
13
|
+
export function selectValue(value, property, decode) {
|
|
10
14
|
const definition = propertyDefinition(property);
|
|
11
15
|
if (definition?.type !== "select") {
|
|
12
16
|
return value;
|
package/dist/authoring.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Schema } from "effect";
|
|
|
2
2
|
import { isAction } from "./action.js";
|
|
3
3
|
import { actionInputToWire } from "./authoring-input.js";
|
|
4
4
|
import { isRecord, nameToWire, permissionToWire } from "./authoring-names.js";
|
|
5
|
+
import { selectValue } from "./authoring-records.js";
|
|
5
6
|
import { isEntity } from "./entity.js";
|
|
6
7
|
import { toCamelCase } from "./names.js";
|
|
7
8
|
import { p, propertyDefinition } from "./property.js";
|
|
@@ -21,7 +22,29 @@ function indexToWire(index) {
|
|
|
21
22
|
const definition = index;
|
|
22
23
|
return { ...definition, properties: (definition.properties ?? []).map(nameToWire) };
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A measure condition with wire names: its property, as `property` or, in a `filtered` expression,
|
|
27
|
+
* `column`, and the values of a select property, which are option values.
|
|
28
|
+
*/
|
|
29
|
+
function conditionToWire(condition, properties) {
|
|
30
|
+
if (!isRecord(condition)) {
|
|
31
|
+
return condition;
|
|
32
|
+
}
|
|
33
|
+
const key = "property" in condition ? "property" : "column";
|
|
34
|
+
const name = condition[key];
|
|
35
|
+
if (typeof name !== "string") {
|
|
36
|
+
return condition;
|
|
37
|
+
}
|
|
38
|
+
const property = isRecord(properties) && Object.hasOwn(properties, name) ? properties[name] : undefined;
|
|
39
|
+
return {
|
|
40
|
+
...condition,
|
|
41
|
+
[key]: nameToWire(name),
|
|
42
|
+
...("value" in condition ? { value: selectValue(condition.value, property, false) } : {}),
|
|
43
|
+
...("values" in condition ? { values: selectValue(condition.values, property, false) } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** A measure expression with wire names: properties, measure references and conditions. */
|
|
47
|
+
function expressionToWire(value, properties) {
|
|
25
48
|
if (!isRecord(value)) {
|
|
26
49
|
return value;
|
|
27
50
|
}
|
|
@@ -30,7 +53,13 @@ function expressionToWire(value) {
|
|
|
30
53
|
expression.name = nameToWire(value.name);
|
|
31
54
|
}
|
|
32
55
|
if (Array.isArray(value.args)) {
|
|
33
|
-
expression.args = value.args.map(expressionToWire);
|
|
56
|
+
expression.args = value.args.map((arg) => expressionToWire(arg, properties));
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value.where)) {
|
|
59
|
+
expression.where = value.where.map((condition) => conditionToWire(condition, properties));
|
|
60
|
+
}
|
|
61
|
+
if (typeof value.balanceOver === "string") {
|
|
62
|
+
expression.balanceOver = nameToWire(value.balanceOver);
|
|
34
63
|
}
|
|
35
64
|
return expression;
|
|
36
65
|
}
|
|
@@ -141,7 +170,7 @@ class AuthoringEncoder {
|
|
|
141
170
|
...(original.measures === undefined
|
|
142
171
|
? {}
|
|
143
172
|
: {
|
|
144
|
-
measures: this.named(original.measures, `${entity.name}.measures`, (value) => expressionToWire(value)),
|
|
173
|
+
measures: this.named(original.measures, `${entity.name}.measures`, (value) => expressionToWire(value, original.properties)),
|
|
145
174
|
}),
|
|
146
175
|
};
|
|
147
176
|
this.definitions.set(original, normalized);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ export { BlobValue, BlobUpload, uploads, BLOB_CONTENT_TYPES, MAX_BLOB_BYTES } fr
|
|
|
2
2
|
export { warehouse, compatibleWarehouseColumn, warehouseColumnType, validateWarehouseSource, } from "./source.js";
|
|
3
3
|
export type { WarehouseSource } from "./source.js";
|
|
4
4
|
export { m } from "./measure.js";
|
|
5
|
-
export type { Measure, Measures, MeasureExpression, MeasureOptions } from "./measure.js";
|
|
5
|
+
export type { AggregateDeclarations, AggregateOptions, Measure, MeasureColumnCondition, MeasureCondition, Measures, MeasureExpression, MeasureOptions, } from "./measure.js";
|
|
6
|
+
export { MEASURE_CONDITION_OPERATORS } from "./aggregate-filters.js";
|
|
7
|
+
export type { AggregateValue, MeasureConditionOperator } from "./aggregate-filters.js";
|
|
6
8
|
export { action, isAction } from "./action.js";
|
|
7
9
|
export type { Action, ActionConfig, ActionConfigWithConnections, ActionContext, ActionEncodedInput, ActionHints, ActionInput, ActionResult, Actions, AnyAction, PrepareContext, PrepareError, } from "./action.js";
|
|
8
10
|
export { CONNECTION_ERROR_REASONS, CONNECTION_KEY, CONNECTION_METHODS, connection, ConnectionError, Connections, ConnectionTransport, isConnection, MAXIMUM_CONNECTION_BODY_BYTES, } from "./connection.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { BlobValue, BlobUpload, uploads, BLOB_CONTENT_TYPES, MAX_BLOB_BYTES } from "./blob.js";
|
|
2
2
|
export { warehouse, compatibleWarehouseColumn, warehouseColumnType, validateWarehouseSource, } from "./source.js";
|
|
3
3
|
export { m } from "./measure.js";
|
|
4
|
+
export { MEASURE_CONDITION_OPERATORS } from "./aggregate-filters.js";
|
|
4
5
|
export { action, isAction } from "./action.js";
|
|
5
6
|
export { CONNECTION_ERROR_REASONS, CONNECTION_KEY, CONNECTION_METHODS, connection, ConnectionError, Connections, ConnectionTransport, isConnection, MAXIMUM_CONNECTION_BODY_BYTES, } from "./connection.js";
|
|
6
7
|
export { Diagnostic, DIAGNOSTIC_CODES, formatDiagnostics, InvalidOntology } from "./diagnostics.js";
|