@segmentstream-cloud/ontology 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 -15
- 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 +1 -1
- package/dist/index.js +1 -1
- 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.d.ts +1 -1
- package/dist/migrate/apply.js +36 -3
- package/dist/migrate/plan.d.ts +1 -1
- package/dist/migrate/plan.js +38 -13
- package/dist/migrate/storage.d.ts +3 -2
- package/dist/migrate/storage.js +15 -5
- package/dist/names.d.ts +4 -2
- package/dist/names.js +4 -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/baseline.d.ts +19 -0
- package/dist/replication/baseline.js +97 -0
- package/dist/replication/bigquery.d.ts +23 -0
- package/dist/replication/bigquery.js +63 -0
- package/dist/replication/model.d.ts +38 -0
- package/dist/replication/model.js +99 -0
- package/dist/replication/schema.d.ts +7 -0
- package/dist/replication/schema.js +123 -0
- package/dist/replication/store.d.ts +35 -0
- package/dist/replication/store.js +88 -0
- package/dist/replication/transaction.d.ts +4 -0
- package/dist/replication/transaction.js +11 -0
- package/dist/runtime/pipeline.js +2 -1
- package/dist/runtime/principals.js +6 -0
- package/dist/runtime/service-binding.js +8 -0
- package/dist/runtime/sql-runtime.js +2 -1
- 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/catalog.d.ts +10 -1
- package/dist/service/catalog.js +12 -3
- package/dist/service/config.d.ts +13 -1
- package/dist/service/config.js +11 -1
- package/dist/service/errors.d.ts +32 -0
- package/dist/service/errors.js +102 -0
- package/dist/service/handlers.d.ts +3 -1
- package/dist/service/handlers.js +67 -29
- package/dist/service/mcp/server.d.ts +5 -0
- package/dist/service/mcp/server.js +61 -9
- package/dist/service/mcp/tools.d.ts +13 -2
- package/dist/service/mcp/tools.js +165 -6
- package/dist/service/operations.d.ts +2 -1
- package/dist/service/operations.js +16 -6
- package/dist/service/plan.js +7 -1
- package/dist/service/replication.d.ts +5 -0
- package/dist/service/replication.js +25 -0
- package/dist/service/runtime-contract.d.ts +1 -0
- package/dist/service/schemas.d.ts +99 -1
- package/dist/service/schemas.js +150 -2
- package/dist/service/service.js +10 -2
- package/dist/user.d.ts +1 -1
- package/dist/warehouse-backed.js +222 -121
- package/package.json +1 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { AGGREGATE_NAME_PATTERN } from "../../aggregate-routes.js";
|
|
2
3
|
import { InvalidOntology } from "../../diagnostics.js";
|
|
4
|
+
import { isValueMatched } from "../../manifest.js";
|
|
3
5
|
import { GENERATED_UPSERT_PREFIX, humanize, mcpToolName } from "../../names.js";
|
|
4
6
|
import { isAllowed, permissionOf, READ_VERB } from "../../permissions.js";
|
|
5
7
|
import { queryJsonSchema } from "../../query.js";
|
|
8
|
+
import { measuresFor, RECORDS_OR_AGGREGATES } from "../../render.js";
|
|
6
9
|
import { actionInputJsonSchema } from "../../runtime/generated-actions.js";
|
|
7
10
|
import { actionDescription, requires } from "../descriptions.js";
|
|
8
|
-
import { HistoryPage, Me } from "../schemas.js";
|
|
11
|
+
import { AGGREGATE_FILTER_OPERATORS, AGGREGATE_GRANULARITIES, AGGREGATE_LIMITS, HistoryPage, Me, } from "../schemas.js";
|
|
9
12
|
const DEFS = "#/$defs/";
|
|
10
13
|
/**
|
|
11
14
|
* A JSON Schema whose root is an object schema, as MCP requires of tool inputs and outputs: a root
|
|
@@ -94,12 +97,30 @@ function relatedHint(planned, traversal) {
|
|
|
94
97
|
`{"${traversal.opposite}": {"some": {"id": {"eq": "${id}"}}}}.`);
|
|
95
98
|
}
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* How an agent reads the target of a value-matched relationship, which the service cannot traverse:
|
|
102
|
+
* the reference is an ordinary property, and the target lives in the warehouse.
|
|
103
|
+
*/
|
|
104
|
+
function matchedHints(planned) {
|
|
105
|
+
return planned.resolved.relationships.flatMap(({ name, relationship, target }) => {
|
|
106
|
+
if (relationship.cardinality !== "one" || !isValueMatched(relationship)) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const property = Object.keys(relationship.on ?? {})[0] ?? "";
|
|
110
|
+
return [
|
|
111
|
+
`${name} is matched on ${property}; read ${target ?? "it"} through the workspace aggregates.`,
|
|
112
|
+
];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
97
115
|
function readTools(planned, schemas, built) {
|
|
98
116
|
const { manifest } = built.plan.ontology;
|
|
99
117
|
const { definition } = planned.resolved.entity;
|
|
100
118
|
const label = definition.label.toLowerCase();
|
|
101
119
|
const read = permissionOf(planned.name, READ_VERB);
|
|
102
|
-
const related =
|
|
120
|
+
const related = [
|
|
121
|
+
...planned.traversals.map((traversal) => relatedHint(planned, traversal)),
|
|
122
|
+
...matchedHints(planned),
|
|
123
|
+
].map((hint) => `- ${hint}`);
|
|
103
124
|
return [
|
|
104
125
|
{
|
|
105
126
|
name: mcpToolName(planned.name, "find"),
|
|
@@ -181,14 +202,140 @@ const GLOBAL_TOOLS = [
|
|
|
181
202
|
target: { kind: "me" },
|
|
182
203
|
},
|
|
183
204
|
];
|
|
205
|
+
const AGGREGATE_NAME = {
|
|
206
|
+
type: "string",
|
|
207
|
+
pattern: AGGREGATE_NAME_PATTERN,
|
|
208
|
+
description: "<entity>.<property>, or a route from a measure's entity when it reaches the property's " +
|
|
209
|
+
"entity in several ways: <entity>.<relationship>.<property>, at most two relationships.",
|
|
210
|
+
};
|
|
211
|
+
const AGGREGATE_VALUE = {
|
|
212
|
+
anyOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
|
|
213
|
+
};
|
|
214
|
+
/** One `anyOf` branch per measure: a JSON Schema `enum` cannot describe its values. */
|
|
215
|
+
function measureChoice({ name, entity, measure }) {
|
|
216
|
+
return {
|
|
217
|
+
const: name,
|
|
218
|
+
description: `${entity.label} · ${measure.label} (${measure.unit}): ${measure.description}`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** The aggregate tool's input for a caller: the request of `POST /api/ontology/aggregate`. */
|
|
222
|
+
function aggregateInput(measures) {
|
|
223
|
+
return {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
measures: {
|
|
227
|
+
type: "array",
|
|
228
|
+
minItems: 1,
|
|
229
|
+
maxItems: AGGREGATE_LIMITS.measures,
|
|
230
|
+
uniqueItems: true,
|
|
231
|
+
items: { anyOf: measures.map(measureChoice) },
|
|
232
|
+
description: "What to compute, as <entity>.<measure>. Measures of several entities are each " +
|
|
233
|
+
"computed over their own entity's rows, then combined on the dimensions.",
|
|
234
|
+
},
|
|
235
|
+
dimensions: {
|
|
236
|
+
type: "array",
|
|
237
|
+
maxItems: AGGREGATE_LIMITS.dimensions,
|
|
238
|
+
uniqueItems: true,
|
|
239
|
+
items: AGGREGATE_NAME,
|
|
240
|
+
description: "What to group by. Every dimension must be reachable from every measure's entity.",
|
|
241
|
+
},
|
|
242
|
+
filters: {
|
|
243
|
+
type: "array",
|
|
244
|
+
maxItems: AGGREGATE_LIMITS.filters,
|
|
245
|
+
description: "All must hold. A filter applies to each measure's entity that reaches it; the answer " +
|
|
246
|
+
"warns about the others.",
|
|
247
|
+
items: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
dimension: AGGREGATE_NAME,
|
|
251
|
+
operator: { enum: [...AGGREGATE_FILTER_OPERATORS] },
|
|
252
|
+
value: { ...AGGREGATE_VALUE, description: "The value of equals." },
|
|
253
|
+
values: {
|
|
254
|
+
type: "array",
|
|
255
|
+
minItems: 1,
|
|
256
|
+
maxItems: AGGREGATE_LIMITS.values,
|
|
257
|
+
items: AGGREGATE_VALUE,
|
|
258
|
+
description: "The values of in, or the two inclusive bounds of between, for example " +
|
|
259
|
+
'["2026-09-01", "2026-09-30"].',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
required: ["dimension", "operator"],
|
|
263
|
+
additionalProperties: false,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
granularity: {
|
|
267
|
+
enum: [...AGGREGATE_GRANULARITIES],
|
|
268
|
+
description: "Buckets the date or timestamp dimension granularityOf names.",
|
|
269
|
+
},
|
|
270
|
+
granularityOf: { ...AGGREGATE_NAME, description: "A requested date or timestamp dimension." },
|
|
271
|
+
orderBy: {
|
|
272
|
+
type: "array",
|
|
273
|
+
maxItems: AGGREGATE_LIMITS.orderBy,
|
|
274
|
+
items: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
measure: { type: "string", description: "A requested measure." },
|
|
278
|
+
dimension: { type: "string", description: "A requested dimension." },
|
|
279
|
+
direction: { enum: ["asc", "desc"] },
|
|
280
|
+
},
|
|
281
|
+
required: ["direction"],
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
limit: {
|
|
286
|
+
type: "integer",
|
|
287
|
+
minimum: 1,
|
|
288
|
+
maximum: AGGREGATE_LIMITS.rows,
|
|
289
|
+
description: "At most 1000 rows; 100 by default.",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
required: ["measures"],
|
|
293
|
+
additionalProperties: false,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const AGGREGATE_DESCRIPTION = "Numbers about the model: measures from one or several entities, grouped and filtered by " +
|
|
297
|
+
`properties of related entities. The rule: ${RECORDS_OR_AGGREGATES}.\n\n` +
|
|
298
|
+
"Examples, with entities sales, stock and product:\n" +
|
|
299
|
+
'- September revenue by brand: {"measures": ["sales.revenue"], "dimensions": ["product.brand"], ' +
|
|
300
|
+
'"filters": [{"dimension": "sales.order_date", "operator": "between", ' +
|
|
301
|
+
'"values": ["2026-09-01", "2026-09-30"]}]}\n' +
|
|
302
|
+
'- September units sold beside units in stock, by status: {"measures": ["sales.units", ' +
|
|
303
|
+
'"stock.on_hand"], "dimensions": ["product.status"], "filters": [{"dimension": ' +
|
|
304
|
+
'"sales.order_date", "operator": "between", "values": ["2026-09-01", "2026-09-30"]}]}\n\n' +
|
|
305
|
+
"Partitioned warehouse entities require a between filter on their date property; the error " +
|
|
306
|
+
"names it. Currency measures need a single-currency filter. " +
|
|
307
|
+
"Answers come from the warehouse, and stored records from its replica: execution.sources says " +
|
|
308
|
+
"how fresh each is, and execution.warnings says when a filter did not apply to a measure. " +
|
|
309
|
+
"Requires reading every entity the request reads, path intermediates included. Call describe " +
|
|
310
|
+
"for the model's measures and relationships.";
|
|
311
|
+
function aggregateTool(manifest) {
|
|
312
|
+
return {
|
|
313
|
+
name: "aggregate",
|
|
314
|
+
title: "Aggregate measures",
|
|
315
|
+
description: AGGREGATE_DESCRIPTION,
|
|
316
|
+
inputSchema: aggregateInput(measuresFor(manifest)),
|
|
317
|
+
inputSchemaFor: (caller) => {
|
|
318
|
+
const measures = measuresFor(manifest, caller.permissions);
|
|
319
|
+
return measures.length === 0 ? undefined : aggregateInput(measures);
|
|
320
|
+
},
|
|
321
|
+
annotations: READ,
|
|
322
|
+
permissions: [],
|
|
323
|
+
target: { kind: "aggregate" },
|
|
324
|
+
};
|
|
325
|
+
}
|
|
184
326
|
/**
|
|
185
|
-
* The MCP tools of an ontology: `describe` and `me`,
|
|
327
|
+
* The MCP tools of an ontology: `describe` and `me`, `aggregate` when the ontology has measures,
|
|
328
|
+
* then for every stored entity `find`, `get`,
|
|
186
329
|
* `history` and a tool per generated and authored action. Entities with a warehouse source have none,
|
|
187
330
|
* as they have no record routes. Throws {@link InvalidOntology} when two tools share a name, which
|
|
188
331
|
* `check` already refuses.
|
|
189
332
|
*/
|
|
190
333
|
export function mcpTools(built) {
|
|
191
|
-
const
|
|
334
|
+
const { manifest } = built.plan.ontology;
|
|
335
|
+
const tools = [
|
|
336
|
+
...GLOBAL_TOOLS,
|
|
337
|
+
...(measuresFor(manifest).length > 0 ? [aggregateTool(manifest)] : []),
|
|
338
|
+
];
|
|
192
339
|
for (const planned of built.plan.entities) {
|
|
193
340
|
if (planned.resolved.entity.definition.source !== undefined) {
|
|
194
341
|
continue;
|
|
@@ -212,7 +359,19 @@ export function mcpTools(built) {
|
|
|
212
359
|
}
|
|
213
360
|
return tools;
|
|
214
361
|
}
|
|
215
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* The tools a caller may see: those whose every permission the caller's roles grant, each with the
|
|
364
|
+
* caller's input schema.
|
|
365
|
+
*/
|
|
216
366
|
export function visibleTools(tools, caller) {
|
|
217
|
-
return tools.
|
|
367
|
+
return tools.flatMap((tool) => {
|
|
368
|
+
if (!tool.permissions.every((required) => isAllowed(required, caller))) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
if (tool.inputSchemaFor === undefined) {
|
|
372
|
+
return [tool];
|
|
373
|
+
}
|
|
374
|
+
const inputSchema = tool.inputSchemaFor(caller);
|
|
375
|
+
return inputSchema === undefined ? [] : [{ ...tool, inputSchema }];
|
|
376
|
+
});
|
|
218
377
|
}
|
|
@@ -4,6 +4,7 @@ import { PermissionDenied } from "../errors.js";
|
|
|
4
4
|
import type { Query, QueryResult } from "../query.js";
|
|
5
5
|
import { CurrentPrincipal } from "./authentication.js";
|
|
6
6
|
import type { AfterCommitHook } from "./config.js";
|
|
7
|
+
import type { ReplicationKick } from "./replication.js";
|
|
7
8
|
import { type ConnectionTransportFactory } from "./connections.js";
|
|
8
9
|
import { InvalidActionInput, RequestId } from "./errors.js";
|
|
9
10
|
import type { ApiPlan, PlannedAction, PlannedEntity, Traversal } from "./plan.js";
|
|
@@ -59,7 +60,7 @@ export interface Caller {
|
|
|
59
60
|
}
|
|
60
61
|
type Backend = OntologyBackend["Service"];
|
|
61
62
|
/** The operations of a planned ontology, bound to its backend, after-commit hooks and connections. */
|
|
62
|
-
export declare function makeOperations(backend: Backend, plan: ApiPlan, hooks: ReadonlyMap<string, AfterCommitHook>, connections: ConnectionTransportFactory): {
|
|
63
|
+
export declare function makeOperations(backend: Backend, plan: ApiPlan, hooks: ReadonlyMap<string, AfterCommitHook>, connections: ConnectionTransportFactory, replicate: ReplicationKick): {
|
|
63
64
|
/** A page of records. Requires `read` on the entity and on every entity the query reaches. */
|
|
64
65
|
find: (planned: PlannedEntity, query: Query) => Effect.Effect<QueryResult<Readonly<Record<string, unknown>>>, PermissionDenied | import("../index.js").InvalidQuery, CurrentPrincipal>;
|
|
65
66
|
/** One record. Requires `read` on the entity. */
|
|
@@ -145,7 +145,7 @@ function traverseOf(backend, targets, planned, traversal, id, query) {
|
|
|
145
145
|
return page(yield* backend.find({ entity: traversal.target, query: { ...query, where } }));
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
|
-
function runActionOf(backend, ontology, hooks, connections) {
|
|
148
|
+
function runActionOf(backend, ontology, hooks, connections, replicate) {
|
|
149
149
|
return (planned, { name, generated }, call) => Effect.gen(function* () {
|
|
150
150
|
const principal = yield* CurrentPrincipal;
|
|
151
151
|
const requestId = yield* RequestId;
|
|
@@ -161,9 +161,19 @@ function runActionOf(backend, ontology, hooks, connections) {
|
|
|
161
161
|
identityToken: call.identityToken,
|
|
162
162
|
audit: { action: `${planned.name}.${name}`, principalId: principal.id, requestId },
|
|
163
163
|
}),
|
|
164
|
-
...(hook
|
|
165
|
-
? {
|
|
166
|
-
|
|
164
|
+
...(hook || backend.replicationEnabled
|
|
165
|
+
? {
|
|
166
|
+
afterCommit: (committed) => Effect.gen(function* () {
|
|
167
|
+
if (hook) {
|
|
168
|
+
yield* afterCommitOf(hook, { requestId, generated }, ontology)(committed);
|
|
169
|
+
}
|
|
170
|
+
if (committed.edits.length > 0 &&
|
|
171
|
+
(yield* backend.replicationEnabled ?? Effect.succeed(false))) {
|
|
172
|
+
yield* replicate(call.identityToken);
|
|
173
|
+
}
|
|
174
|
+
}).pipe(Effect.catchCause((cause) => Effect.logWarning("ontology.replication.deferred", cause))),
|
|
175
|
+
}
|
|
176
|
+
: {}),
|
|
167
177
|
});
|
|
168
178
|
const { issues } = outcome.validation;
|
|
169
179
|
if (!outcome.validation.ok && !call.validateOnly) {
|
|
@@ -199,7 +209,7 @@ function callerOf(backend) {
|
|
|
199
209
|
});
|
|
200
210
|
}
|
|
201
211
|
/** The operations of a planned ontology, bound to its backend, after-commit hooks and connections. */
|
|
202
|
-
export function makeOperations(backend, plan, hooks, connections) {
|
|
212
|
+
export function makeOperations(backend, plan, hooks, connections, replicate) {
|
|
203
213
|
const targets = relationshipTargets(plan);
|
|
204
214
|
return {
|
|
205
215
|
/** A page of records. Requires `read` on the entity and on every entity the query reaches. */
|
|
@@ -211,7 +221,7 @@ export function makeOperations(backend, plan, hooks, connections) {
|
|
|
211
221
|
/** The records a relationship reaches from one record, as a page of the target entity. */
|
|
212
222
|
traverse: (planned, traversal, id, query) => traverseOf(backend, targets, planned, traversal, id, query),
|
|
213
223
|
/** Runs an action as the caller, with the action's after-commit hook and connections. */
|
|
214
|
-
runAction: runActionOf(backend, plan.ontology, hooks, connections),
|
|
224
|
+
runAction: runActionOf(backend, plan.ontology, hooks, connections, replicate),
|
|
215
225
|
/** The caller's user, roles and permissions. It needs no permission. */
|
|
216
226
|
caller: callerOf(backend),
|
|
217
227
|
};
|
package/dist/service/plan.js
CHANGED
|
@@ -63,13 +63,19 @@ export function planApi(ontology, generatedActions) {
|
|
|
63
63
|
const report = (path, message) => {
|
|
64
64
|
diagnostics.push({ severity: "error", code: "reserved-name", path, message });
|
|
65
65
|
};
|
|
66
|
+
// The service cannot read warehouse records, so a relationship to one has no route.
|
|
67
|
+
const warehouseBacked = new Set(ontology.entities
|
|
68
|
+
.filter((candidate) => candidate.entity.definition.source !== undefined)
|
|
69
|
+
.map((candidate) => candidate.name));
|
|
66
70
|
const entities = ontology.entities.map((resolved) => ({
|
|
67
71
|
name: resolved.name,
|
|
68
72
|
resolved,
|
|
69
73
|
traversals: resolved.entity.definition.source !== undefined
|
|
70
74
|
? []
|
|
71
75
|
: [
|
|
72
|
-
...resolved.relationships
|
|
76
|
+
...resolved.relationships
|
|
77
|
+
.filter(({ target }) => target === undefined || !warehouseBacked.has(target))
|
|
78
|
+
.map(declaredTraversal),
|
|
73
79
|
...inverseTraversals(resolved.name, ontology.entities),
|
|
74
80
|
],
|
|
75
81
|
actions: plannedActions(resolved, (entity) => generatedActions(entity, ontology), report),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { OntologyEnvironment } from "./config.js";
|
|
3
|
+
export type ReplicationKick = (identity: string | undefined) => Effect.Effect<void>;
|
|
4
|
+
/** A fast path only. The committed history, not this request, makes delivery durable. */
|
|
5
|
+
export declare function deployedReplication(environment: OntologyEnvironment, fetchImpl?: typeof fetch): ReplicationKick;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
/** A fast path only. The committed history, not this request, makes delivery durable. */
|
|
3
|
+
export function deployedReplication(environment, fetchImpl = fetch) {
|
|
4
|
+
return (identity) => {
|
|
5
|
+
if (!identity) {
|
|
6
|
+
return Effect.void;
|
|
7
|
+
}
|
|
8
|
+
return Effect.tryPromise(async () => {
|
|
9
|
+
const response = await fetchImpl(`${environment.segmentstreamUrl}/api/internal/v1/workspaces/` +
|
|
10
|
+
`${encodeURIComponent(environment.workspaceId)}/ontology/replication/drain`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
redirect: "error",
|
|
13
|
+
signal: AbortSignal.timeout(5000),
|
|
14
|
+
headers: {
|
|
15
|
+
"x-segmentstream-runtime-identity": identity,
|
|
16
|
+
"x-segmentstream-organization-id": environment.organizationId,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
await response.body?.cancel();
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`Replication wake-up returned HTTP ${response.status}.`);
|
|
22
|
+
}
|
|
23
|
+
}).pipe(Effect.catchCause((cause) => Effect.logWarning("ontology.replication.deferred", cause)));
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -189,6 +189,7 @@ declare const OntologyBackend_base: Context.ServiceClass<OntologyBackend, "@segm
|
|
|
189
189
|
readonly resolveApiKey: (key: Redacted.Redacted<string>) => Effect.Effect<Principal | undefined>;
|
|
190
190
|
/** The manifest hash recorded in the `_ontology` catalog table, or `undefined` before the first. */
|
|
191
191
|
readonly appliedHash: Effect.Effect<string | undefined>;
|
|
192
|
+
readonly replicationEnabled?: Effect.Effect<boolean>;
|
|
192
193
|
/** The OAuth tables, for members who connect agents; without it, agents use API keys. */
|
|
193
194
|
readonly oauth?: OAuthStore;
|
|
194
195
|
}>;
|
|
@@ -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",
|