@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 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 needs one on its own
472
- partition property. Currency measures need their entity filtered to one currency, the same
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?: "day" | "week" | "month";
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: "equals" | "in" | "isNull" | "isNotNull";
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
- /** A filter of a model-level aggregate. `between` takes two inclusive `values`, the lower first. */
74
- export interface ModelAggregateFilter<Dimension extends string = string> {
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: "equals" | "in" | "isNull" | "isNotNull" | "between";
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?: "day" | "week" | "month";
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 camelCase per segment;
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
- /** Model-level names keep an `_` before a digit, and the entities in `sources` convert too. */
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.sources;
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: sources.map((source) => ({
274
- ...source,
275
- entity: toReversibleCamelCase(source.entity),
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
  }
@@ -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
@@ -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 || held.entity === wanted.entity) &&
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.11.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",