@segmentstream-cloud/app 0.13.1 → 0.14.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 +56 -13
- package/dist/data.d.ts +8 -1
- package/dist/ontology-wire.d.ts +5 -0
- package/dist/ontology-wire.js +14 -5
- package/dist/ontology.d.ts +15 -4
- package/dist/ontology.js +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,7 +263,7 @@ Records, action inputs and action results are typed from the ontology:
|
|
|
263
263
|
const { items, nextCursor } = await segmentstream.ontology.lead.find({
|
|
264
264
|
where: { status: { in: ["new", "contacted"] } },
|
|
265
265
|
orderBy: [{ field: "createdAt", direction: "desc" }],
|
|
266
|
-
include:
|
|
266
|
+
include: ["owner"],
|
|
267
267
|
limit: 20,
|
|
268
268
|
});
|
|
269
269
|
const lead = await segmentstream.ontology.lead.get(id);
|
|
@@ -279,6 +279,8 @@ const { result, validation, edits } = await segmentstream.ontology.lead.actions.
|
|
|
279
279
|
|
|
280
280
|
- `find` sends the query as URL parameters, `query` as a JSON body; both return
|
|
281
281
|
`{ items, nextCursor }`. Pass `nextCursor` as `after` for the next page.
|
|
282
|
+
- `include` adds related records under their relationship names: list the names,
|
|
283
|
+
`["owner", "tags"]`, or pass `{ owner: true, tags: true }`.
|
|
282
284
|
- `actions.<name>(input, { validateOnly })` posts `{ input, options }` and returns
|
|
283
285
|
`{ result, validation, edits }`. Generated actions are there too
|
|
284
286
|
(`actions.create`, `update`, `delete`), and `action(name, input)` runs any action by
|
|
@@ -444,17 +446,17 @@ reach. Look at or change records through `find` and actions; count, sum or compa
|
|
|
444
446
|
|
|
445
447
|
```ts
|
|
446
448
|
const { fields, rows, execution } = await segmentstream.ontology.aggregate({
|
|
447
|
-
measures: ["
|
|
448
|
-
dimensions: ["
|
|
449
|
+
measures: ["acmeSales.revenue", "acmeStock.onHand"],
|
|
450
|
+
dimensions: ["acmeProduct.assortmentStatus"],
|
|
449
451
|
filters: [
|
|
450
452
|
{
|
|
451
|
-
dimension: "
|
|
453
|
+
dimension: "acmeSales.orderDate",
|
|
452
454
|
operator: "between",
|
|
453
455
|
values: ["2026-09-01", "2026-09-30"],
|
|
454
456
|
},
|
|
455
|
-
{ dimension: "
|
|
457
|
+
{ dimension: "acmeSales.currency", operator: "equals", value: "RUB" },
|
|
456
458
|
],
|
|
457
|
-
orderBy: [{ measure: "
|
|
459
|
+
orderBy: [{ measure: "acmeSales.revenue", direction: "desc" }],
|
|
458
460
|
});
|
|
459
461
|
```
|
|
460
462
|
|
|
@@ -463,8 +465,8 @@ const { fields, rows, execution } = await segmentstream.ontology.aggregate({
|
|
|
463
465
|
`sales_2025.revenue`. This differs from the per-entity call and records, which drop it. Fields
|
|
464
466
|
and rows are keyed by these names.
|
|
465
467
|
- When an entity reaches a property's entity in more than one way, write a route from it:
|
|
466
|
-
`
|
|
467
|
-
`
|
|
468
|
+
`acmeSales.returnedProduct.brand`. Its column is still keyed by the property's own name,
|
|
469
|
+
`acmeProduct.brand`.
|
|
468
470
|
- It posts to the ontology service's `/api/ontology/aggregate` as the signed-in user, who needs
|
|
469
471
|
read permission on every entity the request reaches. Previews and local development answer
|
|
470
472
|
`aggregate_unavailable`.
|
|
@@ -484,13 +486,13 @@ Since 0.13.0, both aggregates take more conditions, buckets and a timezone:
|
|
|
484
486
|
|
|
485
487
|
```ts
|
|
486
488
|
const { rows, execution } = await segmentstream.ontology.aggregate({
|
|
487
|
-
measures: ["
|
|
488
|
-
dimensions: ["
|
|
489
|
+
measures: ["acmeSales.wonOrders"],
|
|
490
|
+
dimensions: ["acmeSales.orderDate"],
|
|
489
491
|
granularity: "week",
|
|
490
|
-
granularityOf: "
|
|
492
|
+
granularityOf: "acmeSales.orderDate",
|
|
491
493
|
filters: [
|
|
492
|
-
{ dimension: "
|
|
493
|
-
{ dimension: "
|
|
494
|
+
{ dimension: "acmeSales.orderDate", operator: "inPeriod", value: "last 4 weeks" },
|
|
495
|
+
{ dimension: "acmeProduct.brand", operator: "notIn", values: ["Unknown", "Test"] },
|
|
494
496
|
],
|
|
495
497
|
timezone: "Europe/Moscow",
|
|
496
498
|
});
|
|
@@ -517,6 +519,40 @@ const { rows, execution } = await segmentstream.ontology.aggregate({
|
|
|
517
519
|
`@segmentstream-cloud/ontology` 0.17.0 or later, whose service accepts them; earlier
|
|
518
520
|
versions refuse them. See [Upgrading to 0.13](#upgrading-to-013).
|
|
519
521
|
|
|
522
|
+
Since 0.14.0, the model-level call also takes the measures of the model, which the ontology
|
|
523
|
+
declares in `defineOntology({ measures })` over measures of several entities, by their bare
|
|
524
|
+
names beside `entity.measure` names:
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
const { fields, rows } = await segmentstream.ontology.aggregate({
|
|
528
|
+
measures: ["planAttainment", "acmeSales.ordered"],
|
|
529
|
+
dimensions: ["acmeSales.orderDate"],
|
|
530
|
+
granularity: "month",
|
|
531
|
+
granularityOf: "acmeSales.orderDate",
|
|
532
|
+
filters: [
|
|
533
|
+
{ dimension: "acmeSales.orderDate", operator: "inPeriod", value: "this year to date" },
|
|
534
|
+
],
|
|
535
|
+
orderBy: [{ measure: "planAttainment", direction: "desc" }],
|
|
536
|
+
});
|
|
537
|
+
// fields[1]: { name: "planAttainment", label: "Plan attainment", unit: "ratio", additive: false,
|
|
538
|
+
// filters: [...], inputs: ["acmeSales.ordered", "acmePlans.plannedUnits"] }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- A measure of the model is requested by the camelCase name the ontology declares, typed from
|
|
542
|
+
`typeof ontology`. The platform names it in snake_case, `plan_attainment`; its field and row
|
|
543
|
+
key are the camelCase form, `planAttainment`, as for entity measures.
|
|
544
|
+
- Its field carries `inputs`, the entity measures it reads, as `entity.measure` in camelCase per
|
|
545
|
+
segment, like other names. They are computed whether or not you request them; request one to
|
|
546
|
+
get its column too.
|
|
547
|
+
- Its `filters` lists the filters that applied to every input. A filter on a property the
|
|
548
|
+
measure aligns applies to each entity through its own property; a filter that reaches some of
|
|
549
|
+
its entities and not others is refused with `invalid_aggregate_query`.
|
|
550
|
+
- A group one of its entities has no rows for, or a zero denominator, gives `null`.
|
|
551
|
+
- The caller needs read permission on every entity it reads.
|
|
552
|
+
- These requests need the workspace's ontology deployed with `@segmentstream-cloud/ontology`
|
|
553
|
+
0.23.0 or later, which declares and serves measures of the model; earlier versions refuse
|
|
554
|
+
them. Requests that name none work as before, and their fields carry no `inputs`.
|
|
555
|
+
|
|
520
556
|
## How requests are handled
|
|
521
557
|
|
|
522
558
|
- Page navigations without a session are redirected to `/sign-in?returnTo=<path>` and
|
|
@@ -627,3 +663,10 @@ Replace segmentstream.semantic with typed warehouse-backed entity aggregates. Se
|
|
|
627
663
|
- The new operators, `inPeriod`, granularities and `timezone` need the workspace's ontology
|
|
628
664
|
deployed with `@segmentstream-cloud/ontology` 0.17.0 or later. Requests that use none of
|
|
629
665
|
them work as before.
|
|
666
|
+
|
|
667
|
+
## Upgrading to 0.14
|
|
668
|
+
|
|
669
|
+
- `segmentstream.ontology.aggregate()` accepts the measures of the model by their bare names, typed
|
|
670
|
+
from an ontology that declares them with `defineOntology({ measures })`. Ontologies without them
|
|
671
|
+
type the same names as before.
|
|
672
|
+
- `AggregateField` gains `inputs`, which only fields of measures of the model carry.
|
package/dist/data.d.ts
CHANGED
|
@@ -115,7 +115,8 @@ export type ModelAggregateFilter<Dimension extends string = string> = {
|
|
|
115
115
|
readonly values?: undefined;
|
|
116
116
|
};
|
|
117
117
|
/**
|
|
118
|
-
* A model-level aggregate: measures of one or more entities, as `entity.measure`,
|
|
118
|
+
* A model-level aggregate: measures of one or more entities, as `entity.measure`, and measures of
|
|
119
|
+
* the model, declared in `defineOntology({ measures })`, by their bare names, grouped by
|
|
119
120
|
* `entity.property` dimensions. A dimension may instead be a route from a measure's entity,
|
|
120
121
|
* `entity.relationship.property`, which picks the way that entity reaches the property. Date bounds
|
|
121
122
|
* are `between` or `inPeriod` filters.
|
|
@@ -156,6 +157,12 @@ export interface AggregateField {
|
|
|
156
157
|
* rows snapshot. The request groups by it or filters it to one day.
|
|
157
158
|
*/
|
|
158
159
|
readonly balanceOver?: string;
|
|
160
|
+
/**
|
|
161
|
+
* On a measure of the model: the entity measures it reads, as `entity.measure`, in declaration
|
|
162
|
+
* order. They are computed whether or not the request names them, and come back as columns only
|
|
163
|
+
* when it does. Its `filters` are the filters that applied to every one of them.
|
|
164
|
+
*/
|
|
165
|
+
readonly inputs?: readonly string[];
|
|
159
166
|
}
|
|
160
167
|
export interface AggregateResult<Row = Record<string, string | boolean | null>> {
|
|
161
168
|
readonly deploymentId: string | null;
|
package/dist/ontology-wire.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export declare function encodeWhere(input: {
|
|
|
46
46
|
readonly getCatalog: () => Promise<WireCatalog>;
|
|
47
47
|
}): Promise<Where | undefined>;
|
|
48
48
|
export declare function queryToWire(query: OntologyQuery): unknown;
|
|
49
|
+
/**
|
|
50
|
+
* `include` with snake_case names, as an object: a list of names, `["assignedOwner"]`, becomes
|
|
51
|
+
* `{ assigned_owner: true }`, which every version of the ontology service accepts.
|
|
52
|
+
*/
|
|
53
|
+
export declare function includeToWire(include: NonNullable<OntologyQuery["include"]>): Readonly<Record<string, unknown>>;
|
|
49
54
|
/** Decode record fields and included relationships without descending into JSON properties. */
|
|
50
55
|
export declare function recordFromWire<A = unknown>(value: unknown, entity: string, catalog: WireCatalog): A;
|
|
51
56
|
export declare function actionInputToWire(input: unknown, entity: string, action: string, catalog: WireCatalog): unknown;
|
package/dist/ontology-wire.js
CHANGED
|
@@ -127,14 +127,23 @@ export function queryToWire(query) {
|
|
|
127
127
|
...(orderBy === undefined
|
|
128
128
|
? {}
|
|
129
129
|
: { order_by: orderBy.map((order) => ({ ...order, field: toSnakeCase(order.field) })) }),
|
|
130
|
-
...(query.include === undefined
|
|
131
|
-
? {}
|
|
132
|
-
: {
|
|
133
|
-
include: Object.fromEntries(Object.entries(query.include).map(([name, value]) => [toSnakeCase(name), value])),
|
|
134
|
-
}),
|
|
130
|
+
...(query.include === undefined ? {} : { include: includeToWire(query.include) }),
|
|
135
131
|
...(where === undefined ? {} : { where }),
|
|
136
132
|
};
|
|
137
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* `include` with snake_case names, as an object: a list of names, `["assignedOwner"]`, becomes
|
|
136
|
+
* `{ assigned_owner: true }`, which every version of the ontology service accepts.
|
|
137
|
+
*/
|
|
138
|
+
export function includeToWire(include) {
|
|
139
|
+
const entries = isIncludeList(include)
|
|
140
|
+
? include.map((name) => [name, true])
|
|
141
|
+
: Object.entries(include);
|
|
142
|
+
return Object.fromEntries(entries.map(([name, value]) => [toSnakeCase(name), value]));
|
|
143
|
+
}
|
|
144
|
+
function isIncludeList(include) {
|
|
145
|
+
return Array.isArray(include);
|
|
146
|
+
}
|
|
138
147
|
function selectToWire(value, property) {
|
|
139
148
|
if (property?.type !== "select") {
|
|
140
149
|
return value;
|
package/dist/ontology.d.ts
CHANGED
|
@@ -53,8 +53,10 @@ export interface OrderBy<Field extends string = string> {
|
|
|
53
53
|
export interface OntologyQuery<Row = JsonRecord> {
|
|
54
54
|
readonly where?: Where;
|
|
55
55
|
readonly orderBy?: ReadonlyArray<OrderBy<keyof Row & string>>;
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Related records to add to each record, by relationship name: `["owner"]` or `{ owner: true }`.
|
|
58
|
+
*/
|
|
59
|
+
readonly include?: ReadonlyArray<string> | {
|
|
58
60
|
readonly [relationship: string]: true;
|
|
59
61
|
};
|
|
60
62
|
/** The `nextCursor` of the previous page. */
|
|
@@ -250,7 +252,16 @@ type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
|
250
252
|
*/
|
|
251
253
|
type CamelCase<S extends string> = S extends `${infer Head}_${infer Tail}` ? Tail extends `${Digit}${string}` ? `${Head}_${CamelCase<Tail>}` : `${Head}${Capitalize<CamelCase<Tail>>}` : S;
|
|
252
254
|
/** `entity.measure` for every measure of every entity, warehouse-backed or stored. */
|
|
253
|
-
type
|
|
255
|
+
type EntityMeasuresOf<E> = E extends unknown ? `${CamelCase<NameOf<E>>}.${MeasuresOf<E>}` : never;
|
|
256
|
+
/**
|
|
257
|
+
* The measures of the model, `defineOntology({ measures })`, by their bare declared names. An
|
|
258
|
+
* ontology SDK without them, or a definition typed as any record of measures, names none.
|
|
259
|
+
*/
|
|
260
|
+
type ModelMeasuresOf<O> = O extends {
|
|
261
|
+
readonly config: {
|
|
262
|
+
readonly measures?: infer M;
|
|
263
|
+
};
|
|
264
|
+
} ? DeclaredNames<NonNullable<M>> : never;
|
|
254
265
|
/** A route picks how an entity with measures reaches a property, so only such entities start one. */
|
|
255
266
|
type RoutesOf<E, O> = [MeasuresOf<E>] extends [never] ? never : PathsOf<E, O>;
|
|
256
267
|
/**
|
|
@@ -273,7 +284,7 @@ type TypedOntologyClient<O> = {
|
|
|
273
284
|
readonly [N in NameOf<EntitiesOf<O>>]: ClientOf<Extract<EntitiesOf<O>, {
|
|
274
285
|
readonly name: N;
|
|
275
286
|
}>, O>;
|
|
276
|
-
} & ModelAggregateClient<
|
|
287
|
+
} & ModelAggregateClient<EntityMeasuresOf<EntitiesOf<O>> | ModelMeasuresOf<O>, ModelDimensionsOf<EntitiesOf<O>, O>>;
|
|
277
288
|
/** A client for any entity, when the app passes no ontology type. */
|
|
278
289
|
export type UntypedOntologyClient = ModelAggregateClient & {
|
|
279
290
|
readonly [entity: string]: EntityClient<JsonRecord, {
|
package/dist/ontology.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SegmentStreamApiError, apiErrorFrom, readJson } from "./errors.js";
|
|
2
|
-
import { actionInputToWire, encodeWhere, editsFromWire, queryToWire, recordFromWire, responseFromWire, } from "./ontology-wire.js";
|
|
2
|
+
import { actionInputToWire, encodeWhere, editsFromWire, includeToWire, queryToWire, recordFromWire, responseFromWire, } from "./ontology-wire.js";
|
|
3
3
|
import { toCamelCase, toReversibleCamelCase, toSnakeCase } from "./ontology-names.js";
|
|
4
4
|
const ENTITY_NAME = /^[a-z][a-zA-Z0-9]*(?:_[a-z0-9]+)*$/;
|
|
5
5
|
const ACTION_NAME = ENTITY_NAME;
|
|
@@ -198,9 +198,11 @@ export function findParameters(query) {
|
|
|
198
198
|
.join(","));
|
|
199
199
|
}
|
|
200
200
|
if (query.include !== undefined) {
|
|
201
|
-
const relationships = Object.
|
|
201
|
+
const relationships = Object.entries(includeToWire(query.include))
|
|
202
|
+
.filter(([, included]) => included === true)
|
|
203
|
+
.map(([name]) => name);
|
|
202
204
|
if (relationships.length > 0) {
|
|
203
|
-
search.set("include", relationships.
|
|
205
|
+
search.set("include", relationships.join(","));
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
208
|
if (query.after !== undefined) {
|
|
@@ -245,8 +247,8 @@ function aggregateToWire(request) {
|
|
|
245
247
|
};
|
|
246
248
|
}
|
|
247
249
|
/**
|
|
248
|
-
* Field names, the filters each measure applied, a balance's dimension
|
|
249
|
-
* camelCase per segment; `execution` is passed on as sent.
|
|
250
|
+
* Field names, the filters each measure applied, a balance's dimension, a model measure's inputs
|
|
251
|
+
* and row keys come back camelCase per segment; `execution` is passed on as sent.
|
|
250
252
|
*/
|
|
251
253
|
function aggregateResultFromWire(result, decodeName) {
|
|
252
254
|
return {
|
|
@@ -256,6 +258,7 @@ function aggregateResultFromWire(result, decodeName) {
|
|
|
256
258
|
name: decodeName(field.name),
|
|
257
259
|
...(field.filters === undefined ? {} : { filters: field.filters.map(decodeName) }),
|
|
258
260
|
...(field.balanceOver === undefined ? {} : { balanceOver: decodeName(field.balanceOver) }),
|
|
261
|
+
...(field.inputs === undefined ? {} : { inputs: field.inputs.map(decodeName) }),
|
|
259
262
|
})),
|
|
260
263
|
rows: result.rows.map((row) => Object.fromEntries(Object.entries(row).map(([name, value]) => [decodeName(name), value]))),
|
|
261
264
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|