@segmentstream-cloud/app 0.4.0 → 0.5.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
@@ -325,10 +325,9 @@ export default async function Page() {
325
325
  - Values arrive as JSON: safe integers as numbers, larger integers and decimals as strings,
326
326
  timestamps as ISO strings, records and arrays as JSON strings.
327
327
 
328
- `segmentstream.semantic` (governed metrics from semantic models) and
329
- `segmentstream.fetch(path)` (any SegmentStream API path) act as the signed-in
330
- SegmentStream member, so they need the legacy sign-in; under Neon Auth they throw
331
- `app_credential_missing`.
328
+ `segmentstream.ontology.<entity>.aggregate(request)` uses the server-held workspace app
329
+ credential and supports Neon Auth apps. Stored record operations use the ontology user token.
330
+ `segmentstream.fetch(path)` still requires the legacy platform session.
332
331
 
333
332
  ## How requests are handled
334
333
 
@@ -387,7 +386,7 @@ Without `NEON_AUTH_BASE_URL`, 0.3 behaves exactly as 0.2, so existing apps keep
387
386
  working: members of the workspace's organization sign in with their SegmentStream
388
387
  account through `/api/segmentstream/[...path]/route.ts`
389
388
  (`export const { GET, POST } = segmentstream.handlers();`), the proxy redirects to
390
- SegmentStream, and `fetch`, `warehouse` and `semantic` act as the member. In that mode
389
+ SegmentStream, and `fetch` acts as the member; warehouse queries and aggregates prefer the workspace app credential. In that mode
391
390
  `roles` and `permissions` are empty, `can()` is `false`, `authHandlers()` answers
392
391
  `404 neon_auth_not_configured`, and `segmentstream.ontology` sends the member's app
393
392
  token, which the ontology service accepts while workspaces move to Neon Auth. Keep the
@@ -410,3 +409,7 @@ app word messages in its own language without matching the English sentences.
410
409
  ## License
411
410
 
412
411
  Apache-2.0
412
+
413
+ ## Upgrading to 0.5
414
+
415
+ Replace segmentstream.semantic with typed warehouse-backed entity aggregates. See [ontology aggregates](../../docs/ontology-aggregates.md). Warehouse-backed clients expose no record CRUD or actions.
package/dist/app.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Environment, SegmentStreamAppConfig } from "./config.js";
2
- import type { SemanticClient, WarehouseClient } from "./data.js";
2
+ import type { WarehouseClient } from "./data.js";
3
3
  import type { SegmentStreamRouteHandlers } from "./handlers.js";
4
4
  import type { AuthRouteHandlers, CreateNeonAuth, SignOutOptions } from "./neon.js";
5
5
  import type { OntologyClient } from "./ontology.js";
@@ -44,8 +44,6 @@ export interface SegmentStreamApp<O = unknown> {
44
44
  * (`SEGMENTSTREAM_APP_CREDENTIAL`) when it is set, else the signed-in member's session.
45
45
  */
46
46
  readonly warehouse: WarehouseClient;
47
- /** Governed metrics from the workspace's deployed semantic models. Server-only. Legacy sign-in only. */
48
- readonly semantic: SemanticClient;
49
47
  /** The app's ontology service, `/api/ontology`, as the signed-in user. Server-only. */
50
48
  readonly ontology: OntologyClient<O>;
51
49
  }
package/dist/app.js CHANGED
@@ -48,13 +48,12 @@ export function createApp(config, dependencies = {}) {
48
48
  getAccessToken: legacy.getAccessToken,
49
49
  fetch: data.fetch,
50
50
  warehouse: data.warehouse,
51
- semantic: data.semantic,
52
51
  // The ontology service accepts SegmentStream app tokens while workspaces move to Neon Auth.
53
52
  ontology: createOntologyClient(createOntologyTransport({
54
53
  fetch: fetchImplementation,
55
54
  ontologyUrl: ontologyUrl === undefined ? undefined : new URL(ontologyUrl).origin,
56
55
  credentials: { token: legacy.getAccessToken },
57
- })),
56
+ }), data.aggregateTransport),
58
57
  };
59
58
  }
60
59
  const neon = createNeonSession({
@@ -82,8 +81,7 @@ export function createApp(config, dependencies = {}) {
82
81
  getAccessToken: async () => null,
83
82
  fetch: data.fetch,
84
83
  warehouse: data.warehouse,
85
- semantic: data.semantic,
86
- ontology: createOntologyClient(neon.ontologyTransport),
84
+ ontology: createOntologyClient(neon.ontologyTransport, data.aggregateTransport),
87
85
  };
88
86
  }
89
87
  /** Under Neon Auth the app has no SegmentStream session to call the API with. */
@@ -91,7 +89,7 @@ async function noSegmentStreamSession() {
91
89
  throw new SegmentStreamApiError({
92
90
  code: "app_credential_missing",
93
91
  status: 401,
94
- detail: "Apps that sign in with Neon Auth have no SegmentStream session. Warehouse queries use the workspace app credential, SEGMENTSTREAM_APP_CREDENTIAL, which is not set; SegmentStream sets it on deploy. Other SegmentStream API calls are not available to these apps yet.",
92
+ detail: "Apps that sign in with Neon Auth have no SegmentStream session. Warehouse queries and ontology aggregates use the workspace app credential, SEGMENTSTREAM_APP_CREDENTIAL, which is not set; SegmentStream sets it on deploy. Other SegmentStream API calls are not available to these apps yet.",
95
93
  });
96
94
  }
97
95
  function notConfiguredHandlers() {
package/dist/data.d.ts CHANGED
@@ -43,38 +43,39 @@ export interface WarehouseDryRun {
43
43
  readonly bytesProcessed: string | null;
44
44
  readonly maximumBytesBilled: string;
45
45
  }
46
- export interface SemanticQueryRequest {
47
- readonly dimensions: readonly string[];
48
- readonly measures: readonly string[];
49
- readonly dateRange: {
46
+ export interface AggregateRequest<Property extends string = string, Measure extends string = string, Temporal extends Property = Property> {
47
+ readonly dimensions: readonly Property[];
48
+ readonly measures: readonly Measure[];
49
+ readonly dateRange?: {
50
50
  readonly from: string;
51
51
  readonly to: string;
52
52
  };
53
+ readonly granularityOf?: Temporal;
53
54
  readonly granularity?: "day" | "week" | "month";
54
55
  readonly filters?: ReadonlyArray<{
55
- readonly dimension: string;
56
+ readonly dimension: Property;
56
57
  readonly operator: "equals" | "in" | "isNull" | "isNotNull";
57
58
  readonly value?: string | number | boolean;
58
59
  readonly values?: ReadonlyArray<string | number | boolean>;
59
60
  }>;
60
61
  readonly orderBy?: ReadonlyArray<{
61
- readonly measure?: string;
62
- readonly dimension?: string;
62
+ readonly measure?: Measure;
63
+ readonly dimension?: Property;
63
64
  readonly direction: "asc" | "desc";
64
65
  }>;
65
66
  readonly limit?: number;
66
67
  }
67
- export interface SemanticField {
68
+ export interface AggregateField {
68
69
  readonly name: string;
69
70
  readonly label: string;
70
71
  readonly type: string;
71
72
  readonly unit?: string;
72
73
  readonly format?: string;
73
74
  }
74
- export interface SemanticQueryResult<Row = Record<string, string | boolean | null>> {
75
+ export interface AggregateResult<Row = Record<string, string | boolean | null>> {
75
76
  readonly deploymentId: string | null;
76
77
  readonly definitionDigest: string;
77
- readonly fields: readonly SemanticField[];
78
+ readonly fields: readonly AggregateField[];
78
79
  readonly rows: Row[];
79
80
  readonly truncated: boolean;
80
81
  readonly execution: {
@@ -94,18 +95,6 @@ export interface WarehouseClient {
94
95
  /** Validates SQL and estimates bytes processed without running it. */
95
96
  readonly dryRun: (sql: string, options?: Pick<WarehouseQueryOptions, "params" | "maximumBytesBilled" | "signal">) => Promise<WarehouseDryRun>;
96
97
  }
97
- export interface SemanticClient {
98
- readonly list: () => Promise<{
99
- readonly deploymentId: string | null;
100
- readonly models: readonly unknown[];
101
- }>;
102
- readonly get: (model: string) => Promise<{
103
- readonly deploymentId: string | null;
104
- readonly definitionDigest: string;
105
- readonly model: unknown;
106
- }>;
107
- readonly query: <Row = Record<string, string | boolean | null>>(model: string, request: SemanticQueryRequest) => Promise<SemanticQueryResult<Row>>;
108
- }
109
98
  export interface DataClientDependencies {
110
99
  readonly config: ResolvedConfig;
111
100
  readonly fetch: Fetch;
@@ -122,5 +111,7 @@ export interface DataClientDependencies {
122
111
  export declare function createDataClient(dependencies: DataClientDependencies): {
123
112
  fetch: (path: string, init?: RequestInit) => Promise<Response>;
124
113
  warehouse: WarehouseClient;
125
- semantic: SemanticClient;
114
+ aggregateTransport: {
115
+ fetch: (path: string, init: RequestInit) => Promise<Response>;
116
+ };
126
117
  };
package/dist/data.js CHANGED
@@ -154,12 +154,14 @@ export function createDataClient(dependencies) {
154
154
  };
155
155
  },
156
156
  };
157
- const semantic = {
158
- list: () => call("GET", `${workspace}/semantic-models`),
159
- get: (model) => call("GET", `${workspace}/semantic-models/${encodeURIComponent(model)}`),
160
- query: (model, request) => call("POST", `${workspace}/semantic-models/${encodeURIComponent(model)}/query`, request),
157
+ const aggregateTransport = {
158
+ fetch: (path, init) => {
159
+ if (!/^\/[a-z][a-z0-9_]*\/aggregate$/.test(path))
160
+ throw new TypeError("Invalid aggregate path.");
161
+ return authorizedFetch(`${workspace}/entities${path}`, init, warehouseToken);
162
+ },
161
163
  };
162
- return { fetch: apiFetch, warehouse, semantic };
164
+ return { fetch: apiFetch, warehouse, aggregateTransport };
163
165
  }
164
166
  function bytesLimit(value) {
165
167
  if (value === undefined) {
package/dist/index.d.ts CHANGED
@@ -22,9 +22,9 @@ export type { AuthenticationMethod, SegmentStreamApp } from "./app.js";
22
22
  export type { ApiIssue } from "./errors.js";
23
23
  export type { CurrentUser } from "./me.js";
24
24
  export type { AuthRouteHandler, AuthRouteHandlers, SignOutOptions } from "./neon.js";
25
- export type { ActionCall, ActionOptions, ActionResponse, Change, Edit, EditKind, EntityClient, FieldFilter, GeneratedActions, HistoryEntry, HistoryOptions, HistoryPage, JsonRecord, JsonValue, ManyFilter, OntologyClient, OntologyPage, OntologyQuery, OrderBy, RequestOptions, UntypedOntologyClient, ValidationIssue, Where, } from "./ontology.js";
25
+ export type { ActionCall, ActionOptions, ActionResponse, Change, Edit, EditKind, EntityClient, FieldFilter, GeneratedActions, HistoryEntry, HistoryOptions, HistoryPage, JsonRecord, JsonValue, ManyFilter, OntologyClient, AggregateEntityClient, OntologyPage, OntologyQuery, OrderBy, RequestOptions, UntypedOntologyClient, ValidationIssue, Where, } from "./ontology.js";
26
26
  export type { PermissionHolder } from "./permissions.js";
27
- export type { SemanticClient, SemanticField, SemanticQueryRequest, SemanticQueryResult, WarehouseClient, WarehouseColumn, WarehouseDryRun, WarehouseQueryOptions, WarehouseReadOptions, WarehouseQueryResult, WarehouseRow, WarehouseValue, } from "./data.js";
27
+ export type { AggregateField, AggregateRequest, AggregateResult, WarehouseClient, WarehouseColumn, WarehouseDryRun, WarehouseQueryOptions, WarehouseReadOptions, WarehouseQueryResult, WarehouseRow, WarehouseValue, } from "./data.js";
28
28
  export type { ParameterType, QueryParameterValue, QueryParameters, TypedParameter, } from "./parameters.js";
29
29
  export type { SegmentStreamAppConfig } from "./config.js";
30
30
  export type { SegmentStreamRouteHandlers, RouteHandler } from "./handlers.js";
@@ -1,3 +1,4 @@
1
+ import type { AggregateRequest, AggregateResult } from "./data.js";
1
2
  /**
2
3
  * The typed client for the app's ontology service, `/api/ontology`. The app imports its ontology as
3
4
  * types only (`import type ontology from "../../ontology"`) and passes `typeof ontology` to
@@ -185,18 +186,30 @@ type ResultOf<A> = A extends {
185
186
  type ActionClients<A> = {
186
187
  readonly [K in keyof A & string]: ActionCall<InputOf<A[K]>, ResultOf<A[K]>>;
187
188
  };
189
+ type PropertiesOf<E> = E extends {
190
+ readonly properties: infer P;
191
+ } ? P : never;
192
+ type MeasuresOf<E> = E extends {
193
+ readonly measures: infer M;
194
+ } ? keyof M & string : never;
195
+ export interface AggregateEntityClient<Property extends string = string, Measure extends string = string, Temporal extends Property = Property> {
196
+ readonly aggregate: (request: AggregateRequest<Property, Measure, Temporal>, options?: RequestOptions) => Promise<AggregateResult>;
197
+ }
198
+ type ClientOf<E> = E extends {
199
+ readonly definition: {
200
+ readonly source: object;
201
+ };
202
+ } ? AggregateEntityClient<keyof PropertiesOf<E> & string, MeasuresOf<E>> : EntityClient<RecordOf<E>, ActionClients<ActionsOf<E>>>;
188
203
  type TypedOntologyClient<O> = {
189
- readonly [N in NameOf<EntitiesOf<O>>]: EntityClient<RecordOf<Extract<EntitiesOf<O>, {
190
- readonly name: N;
191
- }>>, ActionClients<ActionsOf<Extract<EntitiesOf<O>, {
204
+ readonly [N in NameOf<EntitiesOf<O>>]: ClientOf<Extract<EntitiesOf<O>, {
192
205
  readonly name: N;
193
- }>>>>;
206
+ }>>;
194
207
  };
195
208
  /** A client for any entity, when the app passes no ontology type. */
196
209
  export type UntypedOntologyClient = {
197
210
  readonly [entity: string]: EntityClient<JsonRecord, {
198
211
  readonly [action: string]: ActionCall<JsonRecord, JsonValue>;
199
- }>;
212
+ }> & AggregateEntityClient;
200
213
  };
201
214
  /**
202
215
  * `segmentstream.ontology`: one client per entity of `O`, the type of the app's ontology
@@ -211,7 +224,7 @@ export interface OntologyTransport {
211
224
  * Entities and actions exist only in the app's types, so the client resolves them by name when
212
225
  * they are used: `ontology.lead.actions.changeStatus(…)` posts to `lead/actions/changeStatus`.
213
226
  */
214
- export declare function createOntologyClient<O>(transport: OntologyTransport): OntologyClient<O>;
227
+ export declare function createOntologyClient<O>(transport: OntologyTransport, aggregateTransport?: OntologyTransport): OntologyClient<O>;
215
228
  /** `find` sends the query as URL parameters: `where` as JSON, `orderBy` as `field:direction`. */
216
229
  export declare function findParameters(query: OntologyQuery<JsonRecord>): URLSearchParams;
217
230
  export {};
package/dist/ontology.js CHANGED
@@ -19,7 +19,7 @@ const RESERVED = new Set([
19
19
  * Entities and actions exist only in the app's types, so the client resolves them by name when
20
20
  * they are used: `ontology.lead.actions.changeStatus(…)` posts to `lead/actions/changeStatus`.
21
21
  */
22
- export function createOntologyClient(transport) {
22
+ export function createOntologyClient(transport, aggregateTransport) {
23
23
  const entities = new Map();
24
24
  return new Proxy(Object.create(null), {
25
25
  get(_target, property) {
@@ -28,7 +28,14 @@ export function createOntologyClient(transport) {
28
28
  }
29
29
  let client = entities.get(property);
30
30
  if (client === undefined) {
31
- client = entityClient(property, transport);
31
+ client = {
32
+ ...entityClient(property, transport),
33
+ aggregate: (request, options = {}) => {
34
+ if (!aggregateTransport)
35
+ throw new TypeError("An aggregate transport is required.");
36
+ return send(aggregateTransport, "POST", `/${property}/aggregate`, request, options.signal);
37
+ },
38
+ };
32
39
  entities.set(property, client);
33
40
  }
34
41
  return client;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream-cloud/app",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",