@segmentstream-cloud/app 0.11.0 → 0.13.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 +51 -3
- package/dist/data.d.ts +67 -8
- package/dist/index.d.ts +1 -1
- package/dist/ontology.js +25 -9
- package/dist/permissions.d.ts +2 -1
- package/dist/permissions.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -468,9 +468,9 @@ const { fields, rows, execution } = await segmentstream.ontology.aggregate({
|
|
|
468
468
|
- It posts to the ontology service's `/api/ontology/aggregate` as the signed-in user, who needs
|
|
469
469
|
read permission on every entity the request reaches. Previews and local development answer
|
|
470
470
|
`aggregate_unavailable`.
|
|
471
|
-
- Date bounds are `between` filters, inclusive. Each partitioned entity
|
|
472
|
-
partition property. Currency measures need their entity filtered to one
|
|
473
|
-
for every entity.
|
|
471
|
+
- Date bounds are `between` filters, inclusive, or `inPeriod` filters. Each partitioned entity
|
|
472
|
+
needs one on its own partition property. Currency measures need their entity filtered to one
|
|
473
|
+
currency, the same for every entity.
|
|
474
474
|
- A measure field's `filters` lists the filters applied to it, and `execution.warnings` names
|
|
475
475
|
each filter that did not reach a measure's entity. `additive: false` marks a measure whose
|
|
476
476
|
rows don't add up to a total.
|
|
@@ -480,6 +480,43 @@ const { fields, rows, execution } = await segmentstream.ontology.aggregate({
|
|
|
480
480
|
`aggregate_scope_mismatch`. See
|
|
481
481
|
[ontology aggregates](../../docs/ontology-aggregates.md#errors).
|
|
482
482
|
|
|
483
|
+
Since 0.13.0, both aggregates take more conditions, buckets and a timezone:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
const { rows, execution } = await segmentstream.ontology.aggregate({
|
|
487
|
+
measures: ["trendtoysSales.wonOrders"],
|
|
488
|
+
dimensions: ["trendtoysSales.orderDate"],
|
|
489
|
+
granularity: "week",
|
|
490
|
+
granularityOf: "trendtoysSales.orderDate",
|
|
491
|
+
filters: [
|
|
492
|
+
{ dimension: "trendtoysSales.orderDate", operator: "inPeriod", value: "last 4 weeks" },
|
|
493
|
+
{ dimension: "trendtoysProduct.brand", operator: "notIn", values: ["Unknown", "Test"] },
|
|
494
|
+
],
|
|
495
|
+
timezone: "Europe/Moscow",
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
- Filter operators are `equals`, `in`, `notEquals`, `notIn`, `isNull`, `isNotNull`, `gt`,
|
|
500
|
+
`gte`, `lt`, `lte`, `contains`, `startsWith` and `endsWith`; the model-level call adds
|
|
501
|
+
`between` and `inPeriod`. `notEquals` and `notIn` also match nulls and rows whose
|
|
502
|
+
relationship found no target. Text operators are case-sensitive and take no wildcards.
|
|
503
|
+
- `inPeriod` takes a period relative to today, typed as `RelativePeriod`: `today`,
|
|
504
|
+
`yesterday`, `this month`, `last quarter`, `this year to date`, `last 7 days`,
|
|
505
|
+
`last 4 weeks`. `execution.periods` gives the dates each one resolved to and the timezone
|
|
506
|
+
that decided today. Only `between` and `inPeriod` bound a partitioned entity.
|
|
507
|
+
- `granularity` is `hour`, `day`, `week`, `month`, `quarter` or `year`; `hour` needs a
|
|
508
|
+
timestamp dimension.
|
|
509
|
+
- `timezone`, an IANA name or `UTC`, replaces the entities' timezones for timestamp buckets,
|
|
510
|
+
date bounds on timestamps and the today of `inPeriod`.
|
|
511
|
+
- A balance measure's field carries `balanceOver`, the date dimension its rows snapshot.
|
|
512
|
+
Group by that dimension or filter it to one day. `balanceOver` and each period's
|
|
513
|
+
`dimension` come back camelCase per segment, like field names.
|
|
514
|
+
- The per-entity call takes the new operators, granularities and `timezone`, but neither
|
|
515
|
+
`between` nor `inPeriod`: `dateRange` stays its partition bound.
|
|
516
|
+
- These requests need app SDK 0.13.0 and the workspace's ontology deployed with
|
|
517
|
+
`@segmentstream-cloud/ontology` 0.17.0 or later, whose service accepts them; earlier
|
|
518
|
+
versions refuse them. See [Upgrading to 0.13](#upgrading-to-013).
|
|
519
|
+
|
|
483
520
|
## How requests are handled
|
|
484
521
|
|
|
485
522
|
- Page navigations without a session are redirected to `/sign-in?returnTo=<path>` and
|
|
@@ -579,3 +616,14 @@ Replace segmentstream.semantic with typed warehouse-backed entity aggregates. Se
|
|
|
579
616
|
- Deploy the workspace's ontology with `@segmentstream-cloud/ontology` 0.13.0 or later. It
|
|
580
617
|
serves `/api/ontology/<entity>/aggregate` and `/api/ontology/aggregate`; earlier versions
|
|
581
618
|
don't.
|
|
619
|
+
|
|
620
|
+
## Upgrading to 0.13
|
|
621
|
+
|
|
622
|
+
- `ModelAggregateFilter` is now a union: a filter with `value` or `values`, or an `inPeriod`
|
|
623
|
+
filter whose `value` is a `RelativePeriod`. An interface cannot extend a union, so
|
|
624
|
+
`interface X extends ModelAggregateFilter` no longer compiles; write
|
|
625
|
+
`type X = ModelAggregateFilter & { … }` instead. Reading `filter.values` still compiles: it
|
|
626
|
+
is always `undefined` on an `inPeriod` filter.
|
|
627
|
+
- The new operators, `inPeriod`, granularities and `timezone` need the workspace's ontology
|
|
628
|
+
deployed with `@segmentstream-cloud/ontology` 0.17.0 or later. Requests that use none of
|
|
629
|
+
them work as before.
|
package/dist/data.d.ts
CHANGED
|
@@ -43,6 +43,27 @@ export interface WarehouseDryRun {
|
|
|
43
43
|
readonly bytesProcessed: string | null;
|
|
44
44
|
readonly maximumBytesBilled: string;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The filter operators of both aggregates. `equals` and `notEquals` take one `value`; `in` and
|
|
48
|
+
* `notIn` take 1–100 `values`; `isNull` and `isNotNull` take none. `notEquals` and `notIn` also
|
|
49
|
+
* match nulls and rows whose relationship found no target, so they are the exact complements of
|
|
50
|
+
* `equals` and `in`. `gt`, `gte`, `lt` and `lte` compare a date, timestamp or number dimension
|
|
51
|
+
* with one `value`; a date bound on a timestamp covers whole days. `contains`, `startsWith` and
|
|
52
|
+
* `endsWith` match one non-empty string in a text dimension, case-sensitively, without wildcards.
|
|
53
|
+
*/
|
|
54
|
+
export type AggregateFilterOperator = "equals" | "in" | "notEquals" | "notIn" | "isNull" | "isNotNull" | "gt" | "gte" | "lt" | "lte" | "contains" | "startsWith" | "endsWith";
|
|
55
|
+
/** How a date or timestamp dimension is bucketed. `hour` needs a timestamp and yields one. */
|
|
56
|
+
export type AggregateGranularity = "hour" | "day" | "week" | "month" | "quarter" | "year";
|
|
57
|
+
type CalendarUnit = "week" | "month" | "quarter" | "year";
|
|
58
|
+
/**
|
|
59
|
+
* A period relative to today, which the platform resolves to two inclusive dates: `today`,
|
|
60
|
+
* `yesterday`; the whole current or previous week, month, quarter or year (`this month`,
|
|
61
|
+
* `last quarter`); the current one through today (`this year to date`); the N days ending
|
|
62
|
+
* yesterday (`last 7 days`); or the N whole weeks, months, quarters or years before the current
|
|
63
|
+
* one (`last 4 weeks`). N is at least 1, weeks start on Monday, and a period covers at most 3660
|
|
64
|
+
* days.
|
|
65
|
+
*/
|
|
66
|
+
export type RelativePeriod = "today" | "yesterday" | `this ${CalendarUnit}` | `this ${CalendarUnit} to date` | `last ${CalendarUnit}` | `last ${number} ${"days" | "weeks" | "months" | "quarters" | "years"}`;
|
|
46
67
|
/**
|
|
47
68
|
* An aggregate query. Dimensions, filters and ordering name the entity's own properties, or a
|
|
48
69
|
* related entity's property through to-one relationships: `Path`, such as `product.brand`.
|
|
@@ -50,16 +71,22 @@ export interface WarehouseDryRun {
|
|
|
50
71
|
export interface AggregateRequest<Property extends string = string, Measure extends string = string, Temporal extends Property = Property, Path extends string = never> {
|
|
51
72
|
readonly dimensions: readonly (Property | Path)[];
|
|
52
73
|
readonly measures: readonly Measure[];
|
|
74
|
+
/** The only partition bound this request takes: it has neither `between` nor `inPeriod`. */
|
|
53
75
|
readonly dateRange?: {
|
|
54
76
|
readonly from: string;
|
|
55
77
|
readonly to: string;
|
|
56
78
|
};
|
|
57
79
|
/** The selected date or timestamp dimension to bucket, which may be a related one. */
|
|
58
80
|
readonly granularityOf?: Temporal | Path;
|
|
59
|
-
readonly granularity?:
|
|
81
|
+
readonly granularity?: AggregateGranularity;
|
|
82
|
+
/**
|
|
83
|
+
* An IANA timezone, such as `Europe/Moscow`, or `UTC`. It replaces the entities' timezones for
|
|
84
|
+
* timestamp buckets and date bounds on timestamps; measure conditions keep their own.
|
|
85
|
+
*/
|
|
86
|
+
readonly timezone?: string;
|
|
60
87
|
readonly filters?: ReadonlyArray<{
|
|
61
88
|
readonly dimension: Property | Path;
|
|
62
|
-
readonly operator:
|
|
89
|
+
readonly operator: AggregateFilterOperator;
|
|
63
90
|
readonly value?: string | number | boolean;
|
|
64
91
|
readonly values?: ReadonlyArray<string | number | boolean>;
|
|
65
92
|
}>;
|
|
@@ -70,26 +97,42 @@ export interface AggregateRequest<Property extends string = string, Measure exte
|
|
|
70
97
|
}>;
|
|
71
98
|
readonly limit?: number;
|
|
72
99
|
}
|
|
73
|
-
/**
|
|
74
|
-
|
|
100
|
+
/**
|
|
101
|
+
* A filter of a model-level aggregate. Besides the shared operators, `between` takes two inclusive
|
|
102
|
+
* `values`, the lower first, and `inPeriod` a period relative to today as its `value`; the answer's
|
|
103
|
+
* `execution.periods` gives the dates it resolved to. Only these two bound a partitioned entity.
|
|
104
|
+
*/
|
|
105
|
+
export type ModelAggregateFilter<Dimension extends string = string> = {
|
|
75
106
|
readonly dimension: Dimension;
|
|
76
|
-
readonly operator:
|
|
107
|
+
readonly operator: AggregateFilterOperator | "between";
|
|
77
108
|
readonly value?: string | number | boolean;
|
|
78
109
|
readonly values?: ReadonlyArray<string | number | boolean>;
|
|
79
|
-
}
|
|
110
|
+
} | {
|
|
111
|
+
readonly dimension: Dimension;
|
|
112
|
+
readonly operator: "inPeriod";
|
|
113
|
+
readonly value: RelativePeriod;
|
|
114
|
+
/** Never set; declared so code reading `values` from any filter still compiles. */
|
|
115
|
+
readonly values?: undefined;
|
|
116
|
+
};
|
|
80
117
|
/**
|
|
81
118
|
* A model-level aggregate: measures of one or more entities, as `entity.measure`, grouped by
|
|
82
119
|
* `entity.property` dimensions. A dimension may instead be a route from a measure's entity,
|
|
83
120
|
* `entity.relationship.property`, which picks the way that entity reaches the property. Date bounds
|
|
84
|
-
* are `between` filters.
|
|
121
|
+
* are `between` or `inPeriod` filters.
|
|
85
122
|
*/
|
|
86
123
|
export interface ModelAggregateRequest<Measure extends string = string, Dimension extends string = string> {
|
|
87
124
|
readonly measures: readonly Measure[];
|
|
88
125
|
readonly dimensions?: readonly Dimension[];
|
|
89
126
|
readonly filters?: ReadonlyArray<ModelAggregateFilter<Dimension>>;
|
|
90
|
-
readonly granularity?:
|
|
127
|
+
readonly granularity?: AggregateGranularity;
|
|
91
128
|
/** The requested date or timestamp dimension to bucket by `granularity`. */
|
|
92
129
|
readonly granularityOf?: Dimension;
|
|
130
|
+
/**
|
|
131
|
+
* An IANA timezone, such as `Europe/Moscow`, or `UTC`. It replaces the entities' timezones for
|
|
132
|
+
* timestamp buckets, date bounds on timestamps and the today of `inPeriod`; measure conditions
|
|
133
|
+
* keep their own.
|
|
134
|
+
*/
|
|
135
|
+
readonly timezone?: string;
|
|
93
136
|
readonly orderBy?: ReadonlyArray<{
|
|
94
137
|
readonly measure?: Measure;
|
|
95
138
|
readonly dimension?: Dimension;
|
|
@@ -108,6 +151,11 @@ export interface AggregateField {
|
|
|
108
151
|
readonly filters?: readonly string[];
|
|
109
152
|
/** `false` on a measure whose rows don't add up to a total, such as a distinct count or a ratio. */
|
|
110
153
|
readonly additive?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* On a balance measure: the date or timestamp dimension, as `entity.property`, whose days its
|
|
156
|
+
* rows snapshot. The request groups by it or filters it to one day.
|
|
157
|
+
*/
|
|
158
|
+
readonly balanceOver?: string;
|
|
111
159
|
}
|
|
112
160
|
export interface AggregateResult<Row = Record<string, string | boolean | null>> {
|
|
113
161
|
readonly deploymentId: string | null;
|
|
@@ -129,6 +177,17 @@ export interface AggregateResult<Row = Record<string, string | boolean | null>>
|
|
|
129
177
|
readonly entity: string;
|
|
130
178
|
readonly asOf: string;
|
|
131
179
|
}>;
|
|
180
|
+
/**
|
|
181
|
+
* Each `inPeriod` filter in request order: its dimension as requested, its period, the
|
|
182
|
+
* inclusive dates it resolved to and the timezone whose today decided them.
|
|
183
|
+
*/
|
|
184
|
+
readonly periods?: ReadonlyArray<{
|
|
185
|
+
readonly dimension: string;
|
|
186
|
+
readonly value: string;
|
|
187
|
+
readonly from: string;
|
|
188
|
+
readonly to: string;
|
|
189
|
+
readonly timezone: string;
|
|
190
|
+
}>;
|
|
132
191
|
};
|
|
133
192
|
}
|
|
134
193
|
export interface WarehouseClient {
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type { CurrentUser } from "./me.js";
|
|
|
28
28
|
export type { AuthRouteHandler, AuthRouteHandlers, SignOutOptions } from "./neon.js";
|
|
29
29
|
export type { ActionCall, ActionOptions, ActionResponse, Change, Edit, EditKind, EntityClient, FieldFilter, GeneratedActions, HistoryEntry, HistoryOptions, HistoryPage, JsonRecord, JsonValue, ManyFilter, ModelAggregateClient, OntologyClient, AggregateEntityClient, OntologyPage, OntologyQuery, OrderBy, RequestOptions, UntypedOntologyClient, ValidationIssue, Where, } from "./ontology.js";
|
|
30
30
|
export type { PermissionHolder } from "./permissions.js";
|
|
31
|
-
export type { AggregateField, AggregateRequest, AggregateResult, ModelAggregateFilter, ModelAggregateRequest, WarehouseClient, WarehouseColumn, WarehouseDryRun, WarehouseQueryOptions, WarehouseReadOptions, WarehouseQueryResult, WarehouseRow, WarehouseValue, } from "./data.js";
|
|
31
|
+
export type { AggregateField, AggregateFilterOperator, AggregateGranularity, AggregateRequest, AggregateResult, ModelAggregateFilter, ModelAggregateRequest, RelativePeriod, WarehouseClient, WarehouseColumn, WarehouseDryRun, WarehouseQueryOptions, WarehouseReadOptions, WarehouseQueryResult, WarehouseRow, WarehouseValue, } from "./data.js";
|
|
32
32
|
export type { ParameterType, QueryParameterValue, QueryParameters, TypedParameter, } from "./parameters.js";
|
|
33
33
|
export type { SegmentStreamAppConfig } from "./config.js";
|
|
34
34
|
export type { SegmentStreamRouteHandlers, RouteHandler } from "./handlers.js";
|
package/dist/ontology.js
CHANGED
|
@@ -245,8 +245,8 @@ function aggregateToWire(request) {
|
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
/**
|
|
248
|
-
* Field names, the filters each measure applied and row keys come back
|
|
249
|
-
* `execution` is passed on as sent.
|
|
248
|
+
* Field names, the filters each measure applied, a balance's dimension and row keys come back
|
|
249
|
+
* camelCase per segment; `execution` is passed on as sent.
|
|
250
250
|
*/
|
|
251
251
|
function aggregateResultFromWire(result, decodeName) {
|
|
252
252
|
return {
|
|
@@ -255,25 +255,41 @@ function aggregateResultFromWire(result, decodeName) {
|
|
|
255
255
|
...field,
|
|
256
256
|
name: decodeName(field.name),
|
|
257
257
|
...(field.filters === undefined ? {} : { filters: field.filters.map(decodeName) }),
|
|
258
|
+
...(field.balanceOver === undefined ? {} : { balanceOver: decodeName(field.balanceOver) }),
|
|
258
259
|
})),
|
|
259
260
|
rows: result.rows.map((row) => Object.fromEntries(Object.entries(row).map(([name, value]) => [decodeName(name), value]))),
|
|
260
261
|
};
|
|
261
262
|
}
|
|
262
|
-
/**
|
|
263
|
+
/**
|
|
264
|
+
* Model-level names keep an `_` before a digit; the entities in `sources` and the dimensions in
|
|
265
|
+
* `periods` convert too.
|
|
266
|
+
*/
|
|
263
267
|
function modelAggregateResultFromWire(wire) {
|
|
264
268
|
const result = aggregateResultFromWire(wire, toReversibleCamelCase);
|
|
265
|
-
const sources = result.execution
|
|
266
|
-
if (sources === undefined) {
|
|
269
|
+
const { sources, periods } = result.execution;
|
|
270
|
+
if (sources === undefined && periods === undefined) {
|
|
267
271
|
return result;
|
|
268
272
|
}
|
|
269
273
|
return {
|
|
270
274
|
...result,
|
|
271
275
|
execution: {
|
|
272
276
|
...result.execution,
|
|
273
|
-
sources
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
+
...(sources === undefined
|
|
278
|
+
? {}
|
|
279
|
+
: {
|
|
280
|
+
sources: sources.map((source) => ({
|
|
281
|
+
...source,
|
|
282
|
+
entity: toReversibleCamelCase(source.entity),
|
|
283
|
+
})),
|
|
284
|
+
}),
|
|
285
|
+
...(periods === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: {
|
|
288
|
+
periods: periods.map((period) => ({
|
|
289
|
+
...period,
|
|
290
|
+
dimension: toReversibleCamelCase(period.dimension),
|
|
291
|
+
})),
|
|
292
|
+
}),
|
|
277
293
|
},
|
|
278
294
|
};
|
|
279
295
|
}
|
package/dist/permissions.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permissions are `<entity>:<verb>` strings, such as `lead:changeStatus`. A role may use `*` for the
|
|
3
3
|
* entity (`*:read`), the verb (`lead:*`) or both (`*:*`); what an action or a read requires never
|
|
4
|
-
* holds a wildcard.
|
|
4
|
+
* holds a wildcard. A wildcard entity does not cover the built-in `agent_chat`: only a permission
|
|
5
|
+
* that names it does.
|
|
5
6
|
*
|
|
6
7
|
* The matching rule is the ontology service's, copied from `@segmentstream-cloud/ontology`
|
|
7
8
|
* (`src/permissions.ts`) so that the app SDK and its browser components do not depend on the
|
package/dist/permissions.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permissions are `<entity>:<verb>` strings, such as `lead:changeStatus`. A role may use `*` for the
|
|
3
3
|
* entity (`*:read`), the verb (`lead:*`) or both (`*:*`); what an action or a read requires never
|
|
4
|
-
* holds a wildcard.
|
|
4
|
+
* holds a wildcard. A wildcard entity does not cover the built-in `agent_chat`: only a permission
|
|
5
|
+
* that names it does.
|
|
5
6
|
*
|
|
6
7
|
* The matching rule is the ontology service's, copied from `@segmentstream-cloud/ontology`
|
|
7
8
|
* (`src/permissions.ts`) so that the app SDK and its browser components do not depend on the
|
|
@@ -9,6 +10,8 @@
|
|
|
9
10
|
*/
|
|
10
11
|
import { toSnakeCase } from "./ontology-names.js";
|
|
11
12
|
const WILDCARD = "*";
|
|
13
|
+
/** Entities only a permission that names them covers; `*:read` and `*:*` grant nothing on them. */
|
|
14
|
+
const EXPLICIT_GRANT_ENTITIES = new Set(["agent_chat"]);
|
|
12
15
|
/** `<entity>:<verb>` with lowercase snake_case entities and lowercase snake_case verbs, or `*` for either. */
|
|
13
16
|
const PERMISSION = /^(\*|[a-z][a-zA-Z0-9]*(?:_[a-z0-9]+)*):(\*|[a-z][a-zA-Z0-9]*(?:_[a-z0-9]+)*)$/;
|
|
14
17
|
function parsePermission(value) {
|
|
@@ -27,7 +30,9 @@ export function grants(granted, required) {
|
|
|
27
30
|
if (held === undefined || wanted === undefined) {
|
|
28
31
|
return false;
|
|
29
32
|
}
|
|
30
|
-
return ((held.entity === WILDCARD
|
|
33
|
+
return ((held.entity === WILDCARD
|
|
34
|
+
? !EXPLICIT_GRANT_ENTITIES.has(wanted.entity)
|
|
35
|
+
: held.entity === wanted.entity) &&
|
|
31
36
|
(held.verb === WILDCARD || held.verb === wanted.verb));
|
|
32
37
|
}
|
|
33
38
|
/** Whether one of the holder's permissions equals `required` or covers it through a wildcard. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Sign-in, users, permissions, the ontology client and warehouse data access for SegmentStream workspace apps built with Next.js.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|