@segmentstream-cloud/ontology 0.12.0 → 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 +84 -17
- package/dist/aggregate-routes.d.ts +95 -0
- package/dist/aggregate-routes.js +332 -0
- package/dist/analyze.js +83 -16
- package/dist/entity.d.ts +31 -15
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/manifest.d.ts +57 -23
- package/dist/manifest.js +77 -30
- package/dist/measure.d.ts +11 -2
- package/dist/measure.js +14 -1
- package/dist/migrate/apply.js +2 -1
- package/dist/migrate/plan.d.ts +1 -1
- package/dist/migrate/plan.js +59 -22
- package/dist/migrate/storage.d.ts +15 -2
- package/dist/migrate/storage.js +63 -5
- package/dist/names.d.ts +4 -2
- package/dist/names.js +5 -1
- package/dist/query.d.ts +5 -2
- package/dist/query.js +6 -2
- package/dist/relationship.d.ts +36 -5
- package/dist/relationship.js +5 -3
- package/dist/render.d.ts +47 -3
- package/dist/render.js +117 -11
- package/dist/replication/model.d.ts +1 -1
- package/dist/replication/model.js +11 -1
- package/dist/runtime/agents.d.ts +9 -0
- package/dist/runtime/agents.js +145 -0
- package/dist/runtime/service-binding.js +2 -0
- package/dist/service/agent/protocol.d.ts +47 -0
- package/dist/service/agent/protocol.js +59 -0
- package/dist/service/agent/server.d.ts +27 -0
- package/dist/service/agent/server.js +235 -0
- package/dist/service/aggregate-platform.d.ts +60 -0
- package/dist/service/aggregate-platform.js +189 -0
- package/dist/service/aggregate.d.ts +42 -0
- package/dist/service/aggregate.js +145 -0
- package/dist/service/api.d.ts +5 -3
- package/dist/service/api.js +52 -6
- package/dist/service/authentication.d.ts +16 -5
- package/dist/service/authentication.js +51 -6
- package/dist/service/catalog.d.ts +10 -1
- package/dist/service/catalog.js +12 -3
- package/dist/service/config.d.ts +30 -1
- package/dist/service/config.js +53 -1
- package/dist/service/errors.d.ts +38 -0
- package/dist/service/errors.js +118 -0
- package/dist/service/handlers.d.ts +2 -1
- package/dist/service/handlers.js +66 -28
- package/dist/service/mcp/server.d.ts +6 -0
- package/dist/service/mcp/server.js +74 -17
- package/dist/service/mcp/tools.d.ts +13 -2
- package/dist/service/mcp/tools.js +165 -6
- package/dist/service/plan.js +7 -1
- package/dist/service/runtime-contract.d.ts +68 -0
- package/dist/service/runtime-identity.d.ts +59 -0
- package/dist/service/runtime-identity.js +102 -0
- package/dist/service/schemas.d.ts +99 -1
- package/dist/service/schemas.js +150 -2
- package/dist/service/service.d.ts +2 -2
- package/dist/service/service.js +24 -6
- package/dist/user.d.ts +1 -1
- package/dist/warehouse-backed.js +222 -121
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
|
|
3
|
+
/**
|
|
4
|
+
* The header in which a runtime caller names the member it acts for: the member's user id. The
|
|
5
|
+
* service honours it only with a verified runtime token, so nobody else can claim a member with it.
|
|
6
|
+
*/
|
|
7
|
+
export const ACT_AS_HEADER = "x-segmentstream-act-as";
|
|
8
|
+
const CLOCK_TOLERANCE_SECONDS = 5;
|
|
9
|
+
const JWKS_TIMEOUT_MS = 5_000;
|
|
10
|
+
/** The environment that a Vercel function token must name: channels run only in production. */
|
|
11
|
+
const PRODUCTION = "production";
|
|
12
|
+
/** The issuer without a trailing slash, as Vercel writes it in `iss` and as its JWKS URL starts. */
|
|
13
|
+
function normalizedIssuer(issuer) {
|
|
14
|
+
let end = issuer.length;
|
|
15
|
+
while (end > 0 && issuer[end - 1] === "/") {
|
|
16
|
+
end -= 1;
|
|
17
|
+
}
|
|
18
|
+
return issuer.slice(0, end);
|
|
19
|
+
}
|
|
20
|
+
/** The issuer's key set, at `<issuer>/.well-known/jwks` as Vercel publishes it. */
|
|
21
|
+
export function runtimeKeySet(issuer) {
|
|
22
|
+
return createRemoteJWKSet(new URL(`${normalizedIssuer(issuer)}/.well-known/jwks`), {
|
|
23
|
+
timeoutDuration: JWKS_TIMEOUT_MS,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** The key set could not be read: not the token's fault. Every other jose error is about the token. */
|
|
27
|
+
function keySetUnavailable(error) {
|
|
28
|
+
if (!(error instanceof errors.JOSEError)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return (error instanceof errors.JWKSTimeout ||
|
|
32
|
+
error instanceof errors.JWKSInvalid ||
|
|
33
|
+
error.code === errors.JOSEError.code);
|
|
34
|
+
}
|
|
35
|
+
/** What about a token failed, for the operator's log: a jose code or a claim name, never a value. */
|
|
36
|
+
function failureReason(error) {
|
|
37
|
+
if (error instanceof errors.JWTClaimValidationFailed || error instanceof errors.JWTExpired) {
|
|
38
|
+
return `claim:${error.claim}`;
|
|
39
|
+
}
|
|
40
|
+
return error instanceof errors.JOSEError ? error.code : "invalid";
|
|
41
|
+
}
|
|
42
|
+
function claimedIssuer(token) {
|
|
43
|
+
try {
|
|
44
|
+
const { iss } = decodeJwt(token);
|
|
45
|
+
return typeof iss === "string" ? iss : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Verifies the Vercel OIDC token of a function of this deployment's project, as backend-next
|
|
53
|
+
* verifies the ontology service's own: RS256 from the team's issuer, for the team's audience,
|
|
54
|
+
* unexpired, with `project_id` equal to the configured project and `environment` `production`.
|
|
55
|
+
* The issuer and the audience name the team, so no further owner check is needed. A token that
|
|
56
|
+
* claims another issuer is `other`, without a key-set fetch.
|
|
57
|
+
*/
|
|
58
|
+
export function makeRuntimeTokenVerifier(config, keySet) {
|
|
59
|
+
const issuer = normalizedIssuer(config.issuer);
|
|
60
|
+
return async function verifyRuntimeToken(token) {
|
|
61
|
+
if (claimedIssuer(token) !== issuer) {
|
|
62
|
+
return { status: "other" };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const { payload } = await jwtVerify(token, keySet, {
|
|
66
|
+
algorithms: ["RS256"],
|
|
67
|
+
issuer,
|
|
68
|
+
audience: config.audience,
|
|
69
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
70
|
+
requiredClaims: ["exp", "iat", "iss", "aud", "project_id", "environment"],
|
|
71
|
+
});
|
|
72
|
+
if (payload.project_id !== config.projectId) {
|
|
73
|
+
return { status: "invalid", reason: "claim:project_id" };
|
|
74
|
+
}
|
|
75
|
+
if (payload.environment !== PRODUCTION) {
|
|
76
|
+
return { status: "invalid", reason: "claim:environment" };
|
|
77
|
+
}
|
|
78
|
+
return { status: "valid" };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return keySetUnavailable(error)
|
|
82
|
+
? { status: "unavailable" }
|
|
83
|
+
: { status: "invalid", reason: failureReason(error) };
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** The runtime verifier on the issuer's remote key set, which jose caches between requests. */
|
|
88
|
+
export function runtimeTokenVerifier(config) {
|
|
89
|
+
return makeRuntimeTokenVerifier(config, runtimeKeySet(config.issuer));
|
|
90
|
+
}
|
|
91
|
+
/** A runtime token that failed verification: the reason is logged, the token never. */
|
|
92
|
+
export function logRuntimeRejection(reason) {
|
|
93
|
+
return Effect.logWarning("ontology.runtime_identity.rejected").pipe(Effect.annotateLogs({ reason }));
|
|
94
|
+
}
|
|
95
|
+
/** The request's runtime credential, or `undefined` while runtime authentication is disabled. */
|
|
96
|
+
export function runtimeCredential(verify, request) {
|
|
97
|
+
if (verify === undefined) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const actAs = request.headers[ACT_AS_HEADER]?.trim();
|
|
101
|
+
return { verify, actAs: actAs === undefined || actAs === "" ? undefined : actAs };
|
|
102
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { ApiPlan } from "./plan.js";
|
|
|
4
4
|
export declare const IdParam: Schema.String;
|
|
5
5
|
/** The schemas of one entity's records and queries. */
|
|
6
6
|
export interface EntitySchemas {
|
|
7
|
-
/** System fields, properties and `<relationship>_id` per `rel.one`. */
|
|
7
|
+
/** System fields, properties and `<relationship>_id` per stored `rel.one`. */
|
|
8
8
|
readonly record: Schema.Top;
|
|
9
9
|
/** A record with the relationships a query may include. */
|
|
10
10
|
readonly result: Schema.Top;
|
|
@@ -392,6 +392,13 @@ export declare const Graph: Schema.Struct<{
|
|
|
392
392
|
readonly to: Schema.String;
|
|
393
393
|
readonly inverse: Schema.String;
|
|
394
394
|
readonly cardinality: Schema.Literals<readonly ["one", "many"]>;
|
|
395
|
+
/** The property pair of a relationship matched by value: one with a warehouse-backed end. */
|
|
396
|
+
readonly on: Schema.optionalKey<Schema.encodeKeys<Schema.Struct<{
|
|
397
|
+
readonly property: Schema.String;
|
|
398
|
+
readonly targetProperty: Schema.String;
|
|
399
|
+
}>, {
|
|
400
|
+
readonly targetProperty: "target_property";
|
|
401
|
+
}>>;
|
|
395
402
|
readonly optional: Schema.Boolean;
|
|
396
403
|
readonly phrase: Schema.Tuple<readonly [Schema.String, Schema.String]>;
|
|
397
404
|
readonly forward: Schema.String;
|
|
@@ -414,6 +421,92 @@ export declare const SIGN_IN_CHECK_MAX_EMAIL_LENGTH = 320;
|
|
|
414
421
|
export declare const SignInCheckRequest: Schema.Struct<{
|
|
415
422
|
readonly email: Schema.String;
|
|
416
423
|
}>;
|
|
424
|
+
/** The bounds of a model-level aggregate (docs/ontology-aggregates-spec.md, "Request"). */
|
|
425
|
+
export declare const AGGREGATE_LIMITS: {
|
|
426
|
+
readonly measures: 30;
|
|
427
|
+
readonly dimensions: 20;
|
|
428
|
+
readonly filters: 50;
|
|
429
|
+
readonly values: 100;
|
|
430
|
+
readonly orderBy: 50;
|
|
431
|
+
readonly rows: 1000;
|
|
432
|
+
};
|
|
433
|
+
export declare const AGGREGATE_FILTER_OPERATORS: readonly ["equals", "in", "isNull", "isNotNull", "between"];
|
|
434
|
+
/** The operators of the per-entity shorthand, which bounds dates with `dateRange` instead. */
|
|
435
|
+
export declare const ENTITY_AGGREGATE_FILTER_OPERATORS: readonly ["equals", "in", "isNull", "isNotNull"];
|
|
436
|
+
export declare const AGGREGATE_GRANULARITIES: readonly ["day", "week", "month"];
|
|
437
|
+
type AggregateValue = string | number | boolean;
|
|
438
|
+
interface AggregateFilter<Operator extends string> {
|
|
439
|
+
readonly dimension: string;
|
|
440
|
+
readonly operator: Operator;
|
|
441
|
+
readonly value?: AggregateValue;
|
|
442
|
+
readonly values?: ReadonlyArray<AggregateValue>;
|
|
443
|
+
}
|
|
444
|
+
declare const AggregateValue: Schema.Union<readonly [Schema.String, Schema.Number, Schema.Boolean]>;
|
|
445
|
+
interface AggregateOrdering {
|
|
446
|
+
readonly measure?: string;
|
|
447
|
+
readonly dimension?: string;
|
|
448
|
+
readonly direction: "asc" | "desc";
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* `POST /api/ontology/aggregate`. The service checks the shape and the bounds, resolves the names
|
|
452
|
+
* against its manifest and forwards the body unchanged; SegmentStream checks everything else.
|
|
453
|
+
*/
|
|
454
|
+
export declare const AggregateRequest: Schema.Codec<AggregateRequest>;
|
|
455
|
+
export interface AggregateRequest {
|
|
456
|
+
readonly measures: ReadonlyArray<string>;
|
|
457
|
+
readonly dimensions?: ReadonlyArray<string>;
|
|
458
|
+
readonly filters?: ReadonlyArray<AggregateFilter<(typeof AGGREGATE_FILTER_OPERATORS)[number]>>;
|
|
459
|
+
readonly granularity?: (typeof AGGREGATE_GRANULARITIES)[number];
|
|
460
|
+
readonly granularityOf?: string;
|
|
461
|
+
readonly orderBy?: ReadonlyArray<AggregateOrdering>;
|
|
462
|
+
readonly limit?: number;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* `POST /api/ontology/<entity>/aggregate`: SegmentStream's per-entity request, unchanged. Names are
|
|
466
|
+
* relative to the entity (`revenue`, `product.brand`), and `dateRange` bounds a partitioned entity.
|
|
467
|
+
*/
|
|
468
|
+
export declare const EntityAggregateRequest: Schema.Codec<EntityAggregateRequest>;
|
|
469
|
+
export interface EntityAggregateRequest {
|
|
470
|
+
readonly dimensions: ReadonlyArray<string>;
|
|
471
|
+
readonly measures: ReadonlyArray<string>;
|
|
472
|
+
readonly dateRange?: {
|
|
473
|
+
readonly from: string;
|
|
474
|
+
readonly to: string;
|
|
475
|
+
};
|
|
476
|
+
readonly filters?: ReadonlyArray<AggregateFilter<(typeof ENTITY_AGGREGATE_FILTER_OPERATORS)[number]>>;
|
|
477
|
+
readonly granularity?: (typeof AGGREGATE_GRANULARITIES)[number];
|
|
478
|
+
readonly granularityOf?: string;
|
|
479
|
+
readonly orderBy?: ReadonlyArray<AggregateOrdering>;
|
|
480
|
+
readonly limit?: number;
|
|
481
|
+
}
|
|
482
|
+
/** SegmentStream's answer to an aggregate, which the service forwards unchanged. */
|
|
483
|
+
export declare const AggregateResult: Schema.Struct<{
|
|
484
|
+
readonly fields: Schema.$Array<Schema.Struct<{
|
|
485
|
+
readonly name: Schema.String;
|
|
486
|
+
readonly label: Schema.String;
|
|
487
|
+
readonly type: Schema.String;
|
|
488
|
+
readonly unit: Schema.optionalKey<Schema.String>;
|
|
489
|
+
readonly format: Schema.optionalKey<Schema.String>;
|
|
490
|
+
readonly scope: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
491
|
+
readonly implicit: Schema.optionalKey<Schema.Boolean>;
|
|
492
|
+
readonly additive: Schema.optionalKey<Schema.Boolean>;
|
|
493
|
+
readonly filters: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
494
|
+
}>>;
|
|
495
|
+
readonly rows: Schema.$Array<Schema.$Record<Schema.String, Schema.NullOr<Schema.Union<readonly [Schema.String, Schema.Boolean]>>>>;
|
|
496
|
+
readonly truncated: Schema.Boolean;
|
|
497
|
+
readonly execution: Schema.Struct<{
|
|
498
|
+
readonly elapsedMs: Schema.Number;
|
|
499
|
+
readonly bytesProcessed: Schema.optionalKey<Schema.String>;
|
|
500
|
+
readonly bytesBilled: Schema.optionalKey<Schema.String>;
|
|
501
|
+
readonly warnings: Schema.optionalKey<Schema.$Array<Schema.String>>;
|
|
502
|
+
readonly sources: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
503
|
+
readonly entity: Schema.String;
|
|
504
|
+
readonly asOf: Schema.String;
|
|
505
|
+
}>>>;
|
|
506
|
+
}>;
|
|
507
|
+
readonly deploymentId: Schema.NullOr<Schema.String>;
|
|
508
|
+
readonly definitionDigest: Schema.String;
|
|
509
|
+
}>;
|
|
417
510
|
/** Whether the address may sign in, and with a code or a link. */
|
|
418
511
|
export declare const SignInCheckResult: Schema.Union<readonly [Schema.Struct<{
|
|
419
512
|
readonly allowed: Schema.Literal<false>;
|
|
@@ -448,7 +541,12 @@ export declare const ErrorSchemas: {
|
|
|
448
541
|
rateLimited: Schema.Top;
|
|
449
542
|
internal: Schema.Top;
|
|
450
543
|
unavailable: Schema.Top;
|
|
544
|
+
invalidAggregateQuery: Schema.Top;
|
|
545
|
+
readForbidden: Schema.Top;
|
|
546
|
+
aggregateUnavailable: Schema.Top;
|
|
547
|
+
aggregateUpstreamUnavailable: Schema.Top;
|
|
451
548
|
connectionRequired: Schema.Top;
|
|
452
549
|
connectionResponseInvalid: Schema.Top;
|
|
453
550
|
connectionUnavailable: Schema.Top;
|
|
454
551
|
};
|
|
552
|
+
export {};
|
package/dist/service/schemas.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BlobValue } from "../blob.js";
|
|
2
2
|
import { Schema, SchemaTransformation } from "effect";
|
|
3
3
|
import { wireStruct } from "../wire.js";
|
|
4
|
-
import { OntologyManifestWire } from "../manifest.js";
|
|
4
|
+
import { isValueMatched, OntologyManifestWire } from "../manifest.js";
|
|
5
5
|
import { entityQuerySchemas, queryLinks } from "../query.js";
|
|
6
6
|
import { ValidationIssue as RuntimeValidationIssue } from "../runtime/errors.js";
|
|
7
7
|
import { Edit, HistoryEntry as RuntimeHistoryEntry } from "../runtime/history.js";
|
|
@@ -96,7 +96,8 @@ function recordFields(entity) {
|
|
|
96
96
|
fields[name] = definition.optional ? Schema.NullOr(value) : value;
|
|
97
97
|
}
|
|
98
98
|
for (const { name, relationship } of entity.relationships) {
|
|
99
|
-
|
|
99
|
+
// A value-matched relationship's reference is the property its `on` names.
|
|
100
|
+
if (relationship.cardinality === "one" && !isValueMatched(relationship)) {
|
|
100
101
|
const key = Id.annotate({ description: relationship.description });
|
|
101
102
|
fields[`${name}_id`] = relationship.optional ? Schema.NullOr(key) : key;
|
|
102
103
|
}
|
|
@@ -276,6 +277,8 @@ const GraphRelationship = Schema.Struct({
|
|
|
276
277
|
to: Schema.String,
|
|
277
278
|
inverse: Schema.String,
|
|
278
279
|
cardinality: Schema.Literals(["one", "many"]),
|
|
280
|
+
/** The property pair of a relationship matched by value: one with a warehouse-backed end. */
|
|
281
|
+
on: Schema.optionalKey(Schema.Struct({ property: Schema.String, targetProperty: Schema.String }).pipe(Schema.encodeKeys({ targetProperty: "target_property" }))),
|
|
279
282
|
optional: Schema.Boolean,
|
|
280
283
|
phrase: Schema.Tuple([Schema.String, Schema.String]),
|
|
281
284
|
forward: Schema.String.annotate({ examples: ["Lead is owned by one User"] }),
|
|
@@ -304,6 +307,143 @@ export const SignInCheckRequest = Schema.Struct({
|
|
|
304
307
|
description: "The address the person typed, in any case.",
|
|
305
308
|
}),
|
|
306
309
|
}).annotate({ identifier: "SignInCheckRequest" });
|
|
310
|
+
/** The bounds of a model-level aggregate (docs/ontology-aggregates-spec.md, "Request"). */
|
|
311
|
+
export const AGGREGATE_LIMITS = {
|
|
312
|
+
measures: 30,
|
|
313
|
+
dimensions: 20,
|
|
314
|
+
filters: 50,
|
|
315
|
+
values: 100,
|
|
316
|
+
orderBy: 50,
|
|
317
|
+
rows: 1000,
|
|
318
|
+
};
|
|
319
|
+
export const AGGREGATE_FILTER_OPERATORS = [
|
|
320
|
+
"equals",
|
|
321
|
+
"in",
|
|
322
|
+
"isNull",
|
|
323
|
+
"isNotNull",
|
|
324
|
+
"between",
|
|
325
|
+
];
|
|
326
|
+
/** The operators of the per-entity shorthand, which bounds dates with `dateRange` instead. */
|
|
327
|
+
export const ENTITY_AGGREGATE_FILTER_OPERATORS = ["equals", "in", "isNull", "isNotNull"];
|
|
328
|
+
export const AGGREGATE_GRANULARITIES = ["day", "week", "month"];
|
|
329
|
+
const AggregateValue = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]);
|
|
330
|
+
/** Which values each operator takes, with SegmentStream's own messages (`planner.ts`). */
|
|
331
|
+
function filterValuesIssue(filter) {
|
|
332
|
+
const { operator, value, values } = filter;
|
|
333
|
+
switch (operator) {
|
|
334
|
+
case "equals":
|
|
335
|
+
return value === undefined || values !== undefined
|
|
336
|
+
? "Equality filters require one value."
|
|
337
|
+
: undefined;
|
|
338
|
+
case "in":
|
|
339
|
+
return values === undefined ||
|
|
340
|
+
values.length === 0 ||
|
|
341
|
+
values.length > AGGREGATE_LIMITS.values ||
|
|
342
|
+
value !== undefined
|
|
343
|
+
? "Membership filters require between 1 and 100 values."
|
|
344
|
+
: undefined;
|
|
345
|
+
case "between":
|
|
346
|
+
return value !== undefined || values?.length !== 2
|
|
347
|
+
? "between needs two values in order."
|
|
348
|
+
: undefined;
|
|
349
|
+
default:
|
|
350
|
+
return value !== undefined || values !== undefined
|
|
351
|
+
? "Null filters do not accept values."
|
|
352
|
+
: undefined;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function aggregateFilter(operators) {
|
|
356
|
+
const filter = closedStruct({
|
|
357
|
+
dimension: Schema.String.annotate({
|
|
358
|
+
description: "A property, or a path of to-one relationships ending in one.",
|
|
359
|
+
}),
|
|
360
|
+
operator: Schema.Literals(operators),
|
|
361
|
+
value: Schema.optionalKey(AggregateValue).annotate({ description: "The value of equals." }),
|
|
362
|
+
values: Schema.optionalKey(Schema.Array(AggregateValue).check(Schema.isMaxLength(AGGREGATE_LIMITS.values))).annotate({ description: "The values of in, or the two inclusive bounds of between." }),
|
|
363
|
+
}, "Unknown key; use dimension, operator, value and values");
|
|
364
|
+
return filter.check(Schema.makeFilter((candidate) => filterValuesIssue(candidate) ?? true));
|
|
365
|
+
}
|
|
366
|
+
const AggregateOrder = closedStruct({
|
|
367
|
+
measure: Schema.optionalKey(Schema.String),
|
|
368
|
+
dimension: Schema.optionalKey(Schema.String),
|
|
369
|
+
direction: Schema.Literals(["asc", "desc"]),
|
|
370
|
+
}, "Unknown key; use measure or dimension, and direction");
|
|
371
|
+
/** The fields both aggregate requests share, with their bounds. */
|
|
372
|
+
const aggregateFields = {
|
|
373
|
+
granularity: Schema.optionalKey(Schema.Literals(AGGREGATE_GRANULARITIES)),
|
|
374
|
+
granularityOf: Schema.optionalKey(Schema.String).annotate({
|
|
375
|
+
description: "The requested date or timestamp dimension that granularity buckets.",
|
|
376
|
+
}),
|
|
377
|
+
orderBy: Schema.optionalKey(Schema.Array(AggregateOrder).check(Schema.isMaxLength(AGGREGATE_LIMITS.orderBy))),
|
|
378
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: AGGREGATE_LIMITS.rows }))).annotate({ description: "At most 1000 rows; 100 by default." }),
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* `POST /api/ontology/aggregate`. The service checks the shape and the bounds, resolves the names
|
|
382
|
+
* against its manifest and forwards the body unchanged; SegmentStream checks everything else.
|
|
383
|
+
*/
|
|
384
|
+
export const AggregateRequest = closedStruct({
|
|
385
|
+
measures: Schema.Array(Schema.String)
|
|
386
|
+
.check(Schema.isMaxLength(AGGREGATE_LIMITS.measures))
|
|
387
|
+
.annotate({ description: "<entity>.<measure>, at least one." }),
|
|
388
|
+
dimensions: Schema.optionalKey(Schema.Array(Schema.String).check(Schema.isMaxLength(AGGREGATE_LIMITS.dimensions))).annotate({
|
|
389
|
+
description: "<entity>.<property>, or a route from a measure's entity: " +
|
|
390
|
+
"<entity>.<relationship>[.<relationship>].<property>.",
|
|
391
|
+
}),
|
|
392
|
+
filters: Schema.optionalKey(Schema.Array(aggregateFilter(AGGREGATE_FILTER_OPERATORS)).check(Schema.isMaxLength(AGGREGATE_LIMITS.filters))).annotate({ description: "All must hold." }),
|
|
393
|
+
...aggregateFields,
|
|
394
|
+
}, "Unknown key; use measures, dimensions, filters, granularity, granularityOf, orderBy and limit").annotate({ identifier: "AggregateRequest" });
|
|
395
|
+
/**
|
|
396
|
+
* `POST /api/ontology/<entity>/aggregate`: SegmentStream's per-entity request, unchanged. Names are
|
|
397
|
+
* relative to the entity (`revenue`, `product.brand`), and `dateRange` bounds a partitioned entity.
|
|
398
|
+
*/
|
|
399
|
+
export const EntityAggregateRequest = closedStruct({
|
|
400
|
+
dimensions: Schema.Array(Schema.String)
|
|
401
|
+
.check(Schema.isMaxLength(AGGREGATE_LIMITS.dimensions))
|
|
402
|
+
.annotate({
|
|
403
|
+
description: "A property, or a path of to-one relationships: product.brand.",
|
|
404
|
+
}),
|
|
405
|
+
measures: Schema.Array(Schema.String)
|
|
406
|
+
.check(Schema.isMaxLength(AGGREGATE_LIMITS.measures))
|
|
407
|
+
.annotate({ description: "The entity's measures, at least one." }),
|
|
408
|
+
dateRange: Schema.optionalKey(closedStruct({
|
|
409
|
+
from: Schema.String.annotate({ format: "date" }),
|
|
410
|
+
to: Schema.String.annotate({ format: "date" }),
|
|
411
|
+
}, "Unknown key; use from and to")).annotate({ description: "Inclusive days of the entity's partition property." }),
|
|
412
|
+
filters: Schema.optionalKey(Schema.Array(aggregateFilter(ENTITY_AGGREGATE_FILTER_OPERATORS)).check(Schema.isMaxLength(AGGREGATE_LIMITS.filters))).annotate({ description: "All must hold." }),
|
|
413
|
+
...aggregateFields,
|
|
414
|
+
}, "Unknown key; use dimensions, measures, dateRange, filters, granularity, granularityOf, " +
|
|
415
|
+
"orderBy and limit").annotate({ identifier: "EntityAggregateRequest" });
|
|
416
|
+
/** SegmentStream's answer to an aggregate, which the service forwards unchanged. */
|
|
417
|
+
export const AggregateResult = Schema.Struct({
|
|
418
|
+
fields: Schema.Array(Schema.Struct({
|
|
419
|
+
name: Schema.String.annotate({ description: "A dimension identity or <entity>.<measure>." }),
|
|
420
|
+
label: Schema.String,
|
|
421
|
+
type: Schema.String,
|
|
422
|
+
unit: Schema.optionalKey(Schema.String),
|
|
423
|
+
format: Schema.optionalKey(Schema.String),
|
|
424
|
+
scope: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
425
|
+
implicit: Schema.optionalKey(Schema.Boolean),
|
|
426
|
+
additive: Schema.optionalKey(Schema.Boolean).annotate({
|
|
427
|
+
description: "false when the measure's rows do not add up to a total.",
|
|
428
|
+
}),
|
|
429
|
+
filters: Schema.optionalKey(Schema.Array(Schema.String)).annotate({
|
|
430
|
+
description: "The filters the measure applied.",
|
|
431
|
+
}),
|
|
432
|
+
})),
|
|
433
|
+
rows: Schema.Array(Schema.Record(Schema.String, Schema.NullOr(Schema.Union([Schema.String, Schema.Boolean])))),
|
|
434
|
+
truncated: Schema.Boolean,
|
|
435
|
+
execution: Schema.Struct({
|
|
436
|
+
elapsedMs: Schema.Number,
|
|
437
|
+
bytesProcessed: Schema.optionalKey(Schema.String),
|
|
438
|
+
bytesBilled: Schema.optionalKey(Schema.String),
|
|
439
|
+
warnings: Schema.optionalKey(Schema.Array(Schema.String)).annotate({
|
|
440
|
+
description: "For example, a filter that did not apply to some measure's entity.",
|
|
441
|
+
}),
|
|
442
|
+
sources: Schema.optionalKey(Schema.Array(Schema.Struct({ entity: Schema.String, asOf: Schema.String }))).annotate({ description: "How fresh the replica of each stored entity read is." }),
|
|
443
|
+
}),
|
|
444
|
+
deploymentId: Schema.NullOr(Schema.String),
|
|
445
|
+
definitionDigest: Schema.String,
|
|
446
|
+
}).annotate({ identifier: "AggregateResult" });
|
|
307
447
|
/** Whether the address may sign in, and with a code or a link. */
|
|
308
448
|
export const SignInCheckResult = Schema.Union([
|
|
309
449
|
Schema.Struct({ allowed: Schema.Literal(false) }),
|
|
@@ -370,6 +510,14 @@ export const ErrorSchemas = {
|
|
|
370
510
|
rateLimited: errorBody("RateLimited", 429, ["rate_limited"]),
|
|
371
511
|
internal: errorBody("InternalError", 500, ["internal_error"]),
|
|
372
512
|
unavailable: errorBody("ServiceUnavailable", 503, ["service_unavailable"]),
|
|
513
|
+
invalidAggregateQuery: errorBody("InvalidAggregateQuery", 400, ["invalid_aggregate_query"]),
|
|
514
|
+
readForbidden: errorBody("ReadForbidden", 403, ["forbidden"], {
|
|
515
|
+
permissions: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
516
|
+
}),
|
|
517
|
+
aggregateUnavailable: errorBody("AggregateUnavailable", 503, ["aggregate_unavailable"], {
|
|
518
|
+
retryable: Schema.Boolean,
|
|
519
|
+
}),
|
|
520
|
+
aggregateUpstreamUnavailable: errorBody("AggregateUpstreamUnavailable", 502, ["aggregate_upstream_unavailable"], { retryable: Schema.Boolean }),
|
|
373
521
|
connectionRequired: errorBody("ConnectionRequired", 409, ["connection_authorization_required", "connections_not_deployed"], { connection: Schema.String }),
|
|
374
522
|
connectionResponseInvalid: errorBody("ConnectionResponseInvalid", 502, [
|
|
375
523
|
"connection_response_invalid",
|
|
@@ -13,8 +13,8 @@ export interface OntologyService {
|
|
|
13
13
|
readonly dispose: () => Promise<void>;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* The ontology service: the derived HttpApi
|
|
17
|
-
* behind Neon Auth, app-token
|
|
16
|
+
* The ontology service: the derived HttpApi, the MCP server at `/mcp` and the agent's pairing routes,
|
|
17
|
+
* wired to the runtime, behind Neon Auth, app-token, API-key and runtime-token authentication. Every response carries `x-ontology-hash`
|
|
18
18
|
* and `x-request-id`; failures answer with a JSON error and never a stack or SQL.
|
|
19
19
|
*/
|
|
20
20
|
export declare function createOntologyService(ontology: Ontology, config: OntologyServiceConfig): OntologyService;
|
package/dist/service/service.js
CHANGED
|
@@ -4,6 +4,9 @@ import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
|
4
4
|
import { storageMaintenanceAuthenticationLayer } from "../storage/maintenance-auth.js";
|
|
5
5
|
import { nameToWire } from "../authoring-names.js";
|
|
6
6
|
import { sqlServiceRuntime } from "../runtime/service-binding.js";
|
|
7
|
+
import { agentLayer } from "./agent/server.js";
|
|
8
|
+
import { makeAggregates } from "./aggregate.js";
|
|
9
|
+
import { platformAggregates } from "./aggregate-platform.js";
|
|
7
10
|
import { ErrorResponsesLive, buildApi } from "./api.js";
|
|
8
11
|
import { authenticationLayer, neonAuthSessionExchange, tokenVerifier } from "./authentication.js";
|
|
9
12
|
import { OntologyServiceConfigError, } from "./config.js";
|
|
@@ -15,6 +18,7 @@ import { deployedReplication } from "./replication.js";
|
|
|
15
18
|
import { mcpLayer } from "./mcp/server.js";
|
|
16
19
|
import { oauthLayer } from "./oauth/server.js";
|
|
17
20
|
import { planApi } from "./plan.js";
|
|
21
|
+
import { runtimeTokenVerifier } from "./runtime-identity.js";
|
|
18
22
|
/** The header every response carries: the hash of the manifest the service serves. */
|
|
19
23
|
export const ONTOLOGY_HASH_HEADER = "x-ontology-hash";
|
|
20
24
|
/** The header every response carries: the correlation id quoted in error bodies and logs. */
|
|
@@ -57,8 +61,8 @@ function decorate(response, requestId, hash) {
|
|
|
57
61
|
});
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
|
-
* The ontology service: the derived HttpApi
|
|
61
|
-
* behind Neon Auth, app-token
|
|
64
|
+
* The ontology service: the derived HttpApi, the MCP server at `/mcp` and the agent's pairing routes,
|
|
65
|
+
* wired to the runtime, behind Neon Auth, app-token, API-key and runtime-token authentication. Every response carries `x-ontology-hash`
|
|
62
66
|
* and `x-request-id`; failures answer with a JSON error and never a stack or SQL.
|
|
63
67
|
*/
|
|
64
68
|
export function createOntologyService(ontology, config) {
|
|
@@ -74,6 +78,7 @@ export function createOntologyService(ontology, config) {
|
|
|
74
78
|
const hooks = afterCommitHooks(plan, config.afterCommit);
|
|
75
79
|
const verifyToken = tokenVerifier(config);
|
|
76
80
|
const exchangeSession = config.neonAuth === undefined ? undefined : neonAuthSessionExchange(config.neonAuth);
|
|
81
|
+
const verifyRuntime = config.runtimeIdentity === undefined ? undefined : runtimeTokenVerifier(config.runtimeIdentity);
|
|
77
82
|
const backend = runtime.connect({
|
|
78
83
|
ontology,
|
|
79
84
|
databaseUrl: config.databaseUrl,
|
|
@@ -84,15 +89,28 @@ export function createOntologyService(ontology, config) {
|
|
|
84
89
|
? deployedConnections(config)
|
|
85
90
|
: localConnections(config.localConnections));
|
|
86
91
|
const replicate = deployedReplication(config);
|
|
87
|
-
const
|
|
88
|
-
|
|
92
|
+
const aggregates = makeAggregates({
|
|
93
|
+
manifest: ontology.manifest,
|
|
94
|
+
transport: platformAggregates({ ...config, manifestHash: ontology.manifest.hash }),
|
|
95
|
+
});
|
|
96
|
+
const routes = HttpApiBuilder.layer(built.api).pipe(Layer.provide(handlersLayer(built, hooks, connections, replicate, aggregates)), Layer.provide([
|
|
97
|
+
authenticationLayer(verifyToken, exchangeSession, verifyRuntime),
|
|
89
98
|
ErrorResponsesLive,
|
|
90
99
|
storageMaintenanceAuthenticationLayer(config.storageMaintenanceKey),
|
|
91
100
|
]));
|
|
92
101
|
const mcp = config.mcp === false
|
|
93
102
|
? Layer.empty
|
|
94
|
-
: Layer.mergeAll(mcpLayer({
|
|
95
|
-
|
|
103
|
+
: Layer.mergeAll(mcpLayer({
|
|
104
|
+
built,
|
|
105
|
+
hooks,
|
|
106
|
+
connections,
|
|
107
|
+
verifyToken,
|
|
108
|
+
verifyRuntime,
|
|
109
|
+
replicate,
|
|
110
|
+
aggregates,
|
|
111
|
+
}), oauthLayer({ verifyToken }));
|
|
112
|
+
const agent = agentLayer({ verifyToken, verifyRuntime, exchangeSession });
|
|
113
|
+
const application = Layer.mergeAll(routes, mcp, agent, DocsRoute, UnmatchedRoutes).pipe(Layer.provide(backend), Layer.provide(HttpServer.layerServices));
|
|
96
114
|
const web = HttpRouter.toWebHandler(application, { disableLogger: true });
|
|
97
115
|
const hash = ontology.manifest.hash;
|
|
98
116
|
return {
|
package/dist/user.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ export declare const User: import("./entity.js").DraftEntity<"user", {
|
|
|
16
16
|
readonly identityProvider: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
|
|
17
17
|
readonly identitySubject: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
|
|
18
18
|
readonly lastSignInAt: import("effect/Schema").optionalKey<import("./property.js").Property<string, false, false>>;
|
|
19
|
-
}, import("./entity.js").Relationships
|
|
19
|
+
}, import("./entity.js").Relationships, Readonly<Record<never, never>>>;
|
|
20
20
|
export type User = typeof User.Type;
|