@segmentstream-cloud/ontology 0.16.0 → 0.18.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 +70 -1
- package/dist/aggregate-filters.d.ts +48 -0
- package/dist/aggregate-filters.js +252 -0
- package/dist/analyze.js +5 -4
- package/dist/authoring-records.d.ts +5 -0
- package/dist/authoring-records.js +5 -1
- package/dist/authoring.js +32 -3
- package/dist/blob.d.ts +8 -1
- package/dist/blob.js +34 -0
- 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/model.d.ts +12 -8
- package/dist/property.d.ts +4 -3
- package/dist/property.js +6 -1
- package/dist/render.d.ts +6 -3
- package/dist/render.js +158 -2
- package/dist/service/mcp/server.js +2 -2
- package/dist/service/mcp/tools.js +46 -11
- package/dist/service/schemas.d.ts +40 -22
- package/dist/service/schemas.js +46 -43
- package/dist/storage/attachments.js +2 -2
- package/dist/storage/sessions.js +7 -5
- 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,9 +28,22 @@ 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
|
|
|
43
|
+
SDK 0.18.0 lets blob properties store any file. `accept` is optional and can restrict a
|
|
44
|
+
property to exact MIME types or ranges. Existing explicit restrictions keep their meaning.
|
|
45
|
+
See [File storage](#file-storage).
|
|
46
|
+
|
|
34
47
|
SDK 0.12.0 adds warehouse replication enabled by default for all stored business entities,
|
|
35
48
|
fields and business relationships. Existing history captures immutable snapshots and the
|
|
36
49
|
service wakes the publisher after commits. Upgrade and redeploy to activate it; workspace
|
|
@@ -64,6 +77,62 @@ SDK 0.13.0 also serves aggregates. Upgrade notes:
|
|
|
64
77
|
See [ontology aggregates](https://github.com/segmentstream/segmentstream/blob/main/docs/ontology-aggregates.md)
|
|
65
78
|
and [ontology analytics](https://github.com/segmentstream/segmentstream/blob/main/docs/ontology-aggregates-spec.md).
|
|
66
79
|
|
|
80
|
+
## File storage
|
|
81
|
+
|
|
82
|
+
Use `p.blob({ description: "Original source file." })` to retain a file's original bytes.
|
|
83
|
+
Without `accept`, a blob property accepts every concrete MIME type, including custom formats
|
|
84
|
+
and `application/octet-stream`. `p.blob()` also defaults to all files, though ontology checks
|
|
85
|
+
require a description. The manifest publishes this policy as `accept: ["*/*"]`.
|
|
86
|
+
|
|
87
|
+
Restrict a property when its business rules require a format:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
content: p.blob({
|
|
91
|
+
description: "Original POS report or bank statement.",
|
|
92
|
+
accept: [
|
|
93
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
94
|
+
"application/vnd.ms-excel",
|
|
95
|
+
"text/csv",
|
|
96
|
+
"text/tab-separated-values",
|
|
97
|
+
],
|
|
98
|
+
maxBytes: 10 * 1024 * 1024,
|
|
99
|
+
}),
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`accept` takes MIME types, not extensions. Matching ignores case; `image/*` accepts image
|
|
103
|
+
types and `*/*` accepts all types. Empty lists, duplicate entries, invalid MIME types and
|
|
104
|
+
parameters such as `; charset=utf-8` are rejected during ontology checks.
|
|
105
|
+
`BLOB_CONTENT_TYPES` lists common types as a convenience; it is not a global allow-list.
|
|
106
|
+
|
|
107
|
+
| Source format | MIME type |
|
|
108
|
+
| --------------------- | ------------------------------------------------------------------------- |
|
|
109
|
+
| XLSX | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
|
|
110
|
+
| XLS | `application/vnd.ms-excel` |
|
|
111
|
+
| CSV | `text/csv` |
|
|
112
|
+
| TSV | `text/tab-separated-values` |
|
|
113
|
+
| JSON | `application/json` |
|
|
114
|
+
| TXT | `text/plain` |
|
|
115
|
+
| DOCX | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` |
|
|
116
|
+
| ZIP | `application/zip` |
|
|
117
|
+
| Unknown binary format | `application/octet-stream` |
|
|
118
|
+
|
|
119
|
+
Uploads still require a valid concrete MIME type, a nonempty file within the property's
|
|
120
|
+
`maxBytes` and the 25 MiB service limit, available workspace quota and action permissions.
|
|
121
|
+
Completion verifies the stored MIME metadata, exact byte count and upload-session binding.
|
|
122
|
+
PDF, JPEG and PNG retain their prefix-signature checks; plain text retains its UTF-8 prefix
|
|
123
|
+
check. Other formats are stored as opaque bytes, without parsing, conversion, archive
|
|
124
|
+
extraction or malware scanning. A MIME declaration does not prove what a file contains.
|
|
125
|
+
Downloads use attachment disposition; only JPEG and PNG may request inline display.
|
|
126
|
+
Agent attachment promotion honors the same `accept` and `maxBytes` restrictions.
|
|
127
|
+
|
|
128
|
+
Upgrade the ontology SDK to 0.18.0 or later, update any existing `accept` list that should
|
|
129
|
+
allow more types, then check, migrate and redeploy the ontology. The app SDK storage client
|
|
130
|
+
0.13.1 or later uses `application/octet-stream` when the browser supplies no MIME type.
|
|
131
|
+
REST callers should supply that type themselves for unknown formats.
|
|
132
|
+
|
|
133
|
+
See [blob storage](https://github.com/segmentstream/segmentstream/blob/main/docs/ontology-blob-storage-implementation.md)
|
|
134
|
+
for upload tickets, transfer grants and retained revisions.
|
|
135
|
+
|
|
67
136
|
## Define the ontology
|
|
68
137
|
|
|
69
138
|
```ts
|
|
@@ -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
|
+
}
|
package/dist/analyze.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { checkWarehouseSource } from "./warehouse-backed.js";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
3
|
import { BLOB_UPLOAD_ANNOTATION } from "./blob.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isBlobAccept, MAX_BLOB_BYTES } from "./blob.js";
|
|
5
5
|
import { isAction } from "./action.js";
|
|
6
6
|
import { CONNECTION_KEY, isConnection } from "./connection.js";
|
|
7
7
|
import { isEntity } from "./entity.js";
|
|
@@ -411,12 +411,13 @@ function checkProperty(path, schema, definition, report) {
|
|
|
411
411
|
report("invalid-property", path, "Blob properties cannot be unique or have a default.");
|
|
412
412
|
}
|
|
413
413
|
if (!definition.accept?.length ||
|
|
414
|
-
definition.accept.some((type) => !
|
|
415
|
-
new Set(definition.accept)
|
|
414
|
+
definition.accept.some((type) => !isBlobAccept(type)) ||
|
|
415
|
+
new Set(definition.accept.map((type) => type.toLowerCase())).size !==
|
|
416
|
+
definition.accept.length ||
|
|
416
417
|
!Number.isInteger(definition.maxBytes) ||
|
|
417
418
|
definition.maxBytes < 1 ||
|
|
418
419
|
definition.maxBytes > MAX_BLOB_BYTES) {
|
|
419
|
-
report("invalid-constraint", path, "Use
|
|
420
|
+
report("invalid-constraint", path, "Use MIME types or ranges and a limit from 1 byte to 25 MiB.");
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
423
|
if (definition.type === "select") {
|
|
@@ -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/blob.d.ts
CHANGED
|
@@ -20,7 +20,14 @@ export declare const BlobUpload: Schema.Struct<{
|
|
|
20
20
|
export type BlobUpload = typeof BlobUpload.Type;
|
|
21
21
|
export declare const BLOB_UPLOAD_ANNOTATION = "x-ontology-upload";
|
|
22
22
|
export declare const MAX_BLOB_BYTES: number;
|
|
23
|
-
|
|
23
|
+
/** Common content types for file properties; any valid MIME type may be declared. */
|
|
24
|
+
export declare const BLOB_CONTENT_TYPES: readonly ["application/pdf", "image/jpeg", "image/png", "text/plain", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-excel", "text/csv", "text/tab-separated-values", "application/json", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/zip", "application/octet-stream"];
|
|
25
|
+
/** A concrete MIME type, without wildcards, parameters or header control characters. */
|
|
26
|
+
export declare function isBlobContentType(contentType: string): boolean;
|
|
27
|
+
/** A property's MIME restriction: a concrete type, a type range, or all files. */
|
|
28
|
+
export declare function isBlobAccept(accept: string): boolean;
|
|
29
|
+
/** Omitted restrictions accept every concrete MIME type; an empty list accepts nothing. */
|
|
30
|
+
export declare function acceptsBlobContentType(accept: ReadonlyArray<string> | undefined, contentType: string): boolean;
|
|
24
31
|
type SchemaValue<S> = S extends {
|
|
25
32
|
readonly Type: infer T;
|
|
26
33
|
} ? T : S extends Schema.optionalKey<infer Inner> ? Inner["Type"] : never;
|
package/dist/blob.js
CHANGED
|
@@ -28,12 +28,46 @@ function uploadCodec(entity, property) {
|
|
|
28
28
|
export const BlobUpload = Schema.Struct({ uploadId: Schema.String.check(Schema.isUUID()) });
|
|
29
29
|
export const BLOB_UPLOAD_ANNOTATION = "x-ontology-upload";
|
|
30
30
|
export const MAX_BLOB_BYTES = 25 * 1024 * 1024;
|
|
31
|
+
/** Common content types for file properties; any valid MIME type may be declared. */
|
|
31
32
|
export const BLOB_CONTENT_TYPES = [
|
|
32
33
|
"application/pdf",
|
|
33
34
|
"image/jpeg",
|
|
34
35
|
"image/png",
|
|
35
36
|
"text/plain",
|
|
37
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
38
|
+
"application/vnd.ms-excel",
|
|
39
|
+
"text/csv",
|
|
40
|
+
"text/tab-separated-values",
|
|
41
|
+
"application/json",
|
|
42
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
43
|
+
"application/zip",
|
|
44
|
+
"application/octet-stream",
|
|
36
45
|
];
|
|
46
|
+
const CONTENT_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i;
|
|
47
|
+
const CONTENT_TYPE_RANGE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/\*$/i;
|
|
48
|
+
/** A concrete MIME type, without wildcards, parameters or header control characters. */
|
|
49
|
+
export function isBlobContentType(contentType) {
|
|
50
|
+
return contentType === contentType.trim() && CONTENT_TYPE.test(contentType);
|
|
51
|
+
}
|
|
52
|
+
/** A property's MIME restriction: a concrete type, a type range, or all files. */
|
|
53
|
+
export function isBlobAccept(accept) {
|
|
54
|
+
return (accept === accept.trim() &&
|
|
55
|
+
(accept === "*/*" || CONTENT_TYPE_RANGE.test(accept) || isBlobContentType(accept)));
|
|
56
|
+
}
|
|
57
|
+
/** Omitted restrictions accept every concrete MIME type; an empty list accepts nothing. */
|
|
58
|
+
export function acceptsBlobContentType(accept, contentType) {
|
|
59
|
+
if (!isBlobContentType(contentType)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const normalized = contentType.toLowerCase();
|
|
63
|
+
return (accept === undefined ||
|
|
64
|
+
accept.some((candidate) => {
|
|
65
|
+
const pattern = candidate.toLowerCase();
|
|
66
|
+
return (pattern === "*/*" ||
|
|
67
|
+
pattern === normalized ||
|
|
68
|
+
(CONTENT_TYPE_RANGE.test(pattern) && normalized.startsWith(pattern.slice(0, -1))));
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
37
71
|
/** Internal wire codec for session tickets and generated action inputs. */
|
|
38
72
|
export function uploadInput(entity, property) {
|
|
39
73
|
return uploadCodec(entity, property);
|
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";
|