@segmentstream-cloud/ontology 0.15.0 → 0.17.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 +49 -1
- package/dist/aggregate-filters.d.ts +48 -0
- package/dist/aggregate-filters.js +252 -0
- package/dist/authoring-records.d.ts +5 -0
- package/dist/authoring-records.js +5 -1
- package/dist/authoring.js +32 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +109 -60
- package/dist/manifest.js +43 -27
- package/dist/measure.d.ts +86 -7
- package/dist/measure.js +92 -15
- package/dist/migrate/apply.js +2 -0
- package/dist/migrate/model.d.ts +12 -8
- package/dist/migrate/plan.js +14 -4
- package/dist/render.d.ts +6 -3
- package/dist/render.js +158 -2
- package/dist/runtime/service-binding.js +3 -0
- package/dist/service/agent/attachments.d.ts +37 -0
- package/dist/service/agent/attachments.js +225 -0
- package/dist/service/mcp/server.js +28 -5
- package/dist/service/mcp/tools.d.ts +6 -0
- package/dist/service/mcp/tools.js +79 -12
- package/dist/service/runtime-contract.d.ts +3 -0
- package/dist/service/schemas.d.ts +40 -22
- package/dist/service/schemas.js +46 -43
- package/dist/service/service.js +11 -1
- package/dist/storage/attachments.d.ts +117 -0
- package/dist/storage/attachments.js +536 -0
- package/dist/storage/maintenance.d.ts +4 -1
- package/dist/storage/maintenance.js +7 -2
- package/dist/storage/neon.js +14 -0
- package/dist/storage/provider.d.ts +2 -0
- package/dist/storage/tables.d.ts +8 -0
- package/dist/storage/tables.js +32 -0
- package/dist/warehouse-backed.js +199 -6
- package/package.json +1 -1
package/dist/warehouse-backed.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dimensionTypeOf, filterTypeIssue, filterValuesIssue, MEASURE_CONDITION_OPERATORS, } from "./aggregate-filters.js";
|
|
2
|
+
import { measuresManifest } from "./manifest.js";
|
|
3
|
+
import { balanceOf, conditionProperty, conditionsOf, MEASURE_AGGREGATES, } from "./measure.js";
|
|
4
|
+
import { humanize, isSystemProperty, SNAKE_CASE_NAME } from "./names.js";
|
|
2
5
|
const columnName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
6
|
const types = new Set(["text", "integer", "number", "boolean", "date", "timestamp", "select"]);
|
|
4
7
|
/**
|
|
@@ -92,23 +95,135 @@ export function checkWarehouseSource(entities, report) {
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The aggregates of one property, which take one argument. `countDistinct` counts what compares
|
|
100
|
+
* exactly; `sum`, `avg`, `min` and `max` take numbers.
|
|
101
|
+
*/
|
|
102
|
+
const AGGREGATES = new Set(["sum", "countDistinct", "avg", "min", "max"]);
|
|
97
103
|
const OPERATORS = new Set([...AGGREGATES, "add", "subtract", "multiply", "divide"]);
|
|
104
|
+
/** The most conditions one aggregate takes. */
|
|
105
|
+
const MAXIMUM_CONDITIONS = 10;
|
|
106
|
+
const CONDITION_OPERATORS = new Set(MEASURE_CONDITION_OPERATORS);
|
|
107
|
+
const CONDITION_PROPERTIES = "Measure conditions name properties of the entity.";
|
|
98
108
|
/**
|
|
99
109
|
* The most dimensions and measures the platform accepts in one aggregate model. It also refuses a
|
|
100
|
-
* model definition over
|
|
110
|
+
* model definition over {@link MAXIMUM_MODEL_BYTES}.
|
|
101
111
|
*/
|
|
102
112
|
const MAXIMUM_MODEL_MEMBERS = 100;
|
|
103
113
|
/** Stored property types that are not dimensions: their values cannot be grouped by. */
|
|
104
114
|
const UNGROUPABLE_TYPES = new Set(["json", "blob"]);
|
|
115
|
+
/** The most JSON the platform reads as one entity's model: its dimensions and measures. */
|
|
116
|
+
const MAXIMUM_MODEL_BYTES = 60 * 1024;
|
|
117
|
+
/**
|
|
118
|
+
* An entity's model as the platform decodes it (`warehouse-backed-entities.ts`): a warehouse-backed
|
|
119
|
+
* entity's properties as dimensions, or a stored entity's id and groupable properties over its
|
|
120
|
+
* replication view, and its measures as the manifest writes them.
|
|
121
|
+
*/
|
|
122
|
+
function modelDefinition(entity) {
|
|
123
|
+
const { definition } = entity.entity;
|
|
124
|
+
const { source } = definition;
|
|
125
|
+
const label = (name, property) => property.label ?? humanize(name);
|
|
126
|
+
const identity = {
|
|
127
|
+
name: entity.name,
|
|
128
|
+
label: definition.label,
|
|
129
|
+
description: definition.description,
|
|
130
|
+
};
|
|
131
|
+
const measures = measuresManifest(entity);
|
|
132
|
+
if (source === undefined) {
|
|
133
|
+
const entityLabel = typeof definition.label === "string" ? definition.label.toLowerCase() : "";
|
|
134
|
+
const id = {
|
|
135
|
+
name: "id",
|
|
136
|
+
column: "id",
|
|
137
|
+
type: "string",
|
|
138
|
+
label: "ID",
|
|
139
|
+
description: `The ${entityLabel}'s id.`,
|
|
140
|
+
scope: [],
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
...identity,
|
|
144
|
+
source: `ontology/${entity.name}`,
|
|
145
|
+
dimensions: [
|
|
146
|
+
id,
|
|
147
|
+
...entity.properties.flatMap(({ name, definition: property }) => {
|
|
148
|
+
const type = dimensionTypeOf(property.type);
|
|
149
|
+
return type === undefined
|
|
150
|
+
? []
|
|
151
|
+
: [
|
|
152
|
+
{
|
|
153
|
+
name,
|
|
154
|
+
column: name,
|
|
155
|
+
type,
|
|
156
|
+
label: label(name, property),
|
|
157
|
+
description: property.description ?? "",
|
|
158
|
+
scope: [],
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}),
|
|
162
|
+
],
|
|
163
|
+
measures,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...identity,
|
|
168
|
+
source: source.asset,
|
|
169
|
+
...(source.currency ? { currencyDimension: source.currency } : {}),
|
|
170
|
+
dimensions: entity.properties.map(({ name, definition: property }) => ({
|
|
171
|
+
name,
|
|
172
|
+
label: label(name, property),
|
|
173
|
+
description: property.description ?? "",
|
|
174
|
+
type: dimensionTypeOf(property.type),
|
|
175
|
+
column: property.column,
|
|
176
|
+
scope: property.scope ?? [],
|
|
177
|
+
})),
|
|
178
|
+
measures,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function isRecord(value) {
|
|
182
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
183
|
+
}
|
|
184
|
+
/** Whether an expression is an aggregate: `count`, or an aggregate of one property. */
|
|
185
|
+
function isAggregate(expression) {
|
|
186
|
+
return MEASURE_AGGREGATES.includes(expression.op);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Why a measure condition is refused, or `undefined`. It names a property of the entity, never a
|
|
190
|
+
* path, with an operator and values a request filter on that property would take, and never a
|
|
191
|
+
* period relative to today.
|
|
192
|
+
*/
|
|
193
|
+
function conditionIssue(condition, properties) {
|
|
194
|
+
if (!isRecord(condition)) {
|
|
195
|
+
return CONDITION_PROPERTIES;
|
|
196
|
+
}
|
|
197
|
+
const property = conditionProperty(condition);
|
|
198
|
+
const definition = typeof property === "string" ? properties.get(property) : undefined;
|
|
199
|
+
const type = definition === undefined ? undefined : dimensionTypeOf(definition.type);
|
|
200
|
+
const { operator, value, values } = condition;
|
|
201
|
+
if (type === undefined) {
|
|
202
|
+
return CONDITION_PROPERTIES;
|
|
203
|
+
}
|
|
204
|
+
if (operator === "inPeriod") {
|
|
205
|
+
return "Measure conditions cannot be relative to today.";
|
|
206
|
+
}
|
|
207
|
+
if (typeof operator !== "string" || !CONDITION_OPERATORS.has(operator)) {
|
|
208
|
+
return `Measure conditions use ${MEASURE_CONDITION_OPERATORS.join(", ")}.`;
|
|
209
|
+
}
|
|
210
|
+
// Values that are not a list are as wrong as an empty list, which no operator takes.
|
|
211
|
+
const listed = values === undefined || Array.isArray(values) ? values : [];
|
|
212
|
+
const filter = { operator, value, values: listed };
|
|
213
|
+
return filterValuesIssue(filter) ?? filterTypeIssue(filter, type);
|
|
214
|
+
}
|
|
105
215
|
/**
|
|
106
216
|
* The measures of a warehouse-backed or stored entity. A measure's columns name properties of the
|
|
107
217
|
* entity. `hasCurrency` says whether the entity maps a text `source.currency`, which stored entities
|
|
108
218
|
* never do, so their measures cannot be currency amounts.
|
|
109
219
|
*/
|
|
110
220
|
function checkMeasures(input) {
|
|
111
|
-
const { entity, measures, properties, hasCurrency
|
|
221
|
+
const { entity, measures, properties, hasCurrency } = input;
|
|
222
|
+
let refusals = 0;
|
|
223
|
+
const report = (code, path, message) => {
|
|
224
|
+
refusals++;
|
|
225
|
+
input.report(code, path, message);
|
|
226
|
+
};
|
|
112
227
|
// A stored entity's rows carry its system fields, whose names a measure cannot take.
|
|
113
228
|
const stored = entity.entity.definition.source === undefined;
|
|
114
229
|
const measureCount = Object.keys(measures).length;
|
|
@@ -139,6 +254,35 @@ function checkMeasures(input) {
|
|
|
139
254
|
report("invalid-measure", field, "Invalid measure format.");
|
|
140
255
|
}
|
|
141
256
|
let remaining = 10_000;
|
|
257
|
+
/** The balance properties of the measure's aggregates and of the measures it references. */
|
|
258
|
+
const balances = new Set();
|
|
259
|
+
/** An aggregate's balance property, which must be a date or timestamp property. */
|
|
260
|
+
const checkBalance = (balance) => {
|
|
261
|
+
const type = typeof balance === "string" ? properties.get(balance)?.type : undefined;
|
|
262
|
+
if (type !== "date" && type !== "timestamp") {
|
|
263
|
+
report("invalid-measure", field, "balanceOver names a date or timestamp property.");
|
|
264
|
+
}
|
|
265
|
+
if (typeof balance === "string") {
|
|
266
|
+
balances.add(balance);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
/** An aggregate's conditions, from its `where` option or the `filtered` expression around it. */
|
|
270
|
+
const checkConditions = (where) => {
|
|
271
|
+
if (!Array.isArray(where) || where.length === 0) {
|
|
272
|
+
report("invalid-measure", field, "Measure conditions take at least one entry.");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (where.length > MAXIMUM_CONDITIONS) {
|
|
276
|
+
report("invalid-measure", field, "Measure conditions take at most 10 entries.");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
for (const condition of where) {
|
|
280
|
+
const issue = conditionIssue(condition, properties);
|
|
281
|
+
if (issue !== undefined) {
|
|
282
|
+
report("invalid-measure", field, issue);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
142
286
|
/** `aggregate` is the aggregate the expression is an argument of, if any. */
|
|
143
287
|
const visit = (expression, references, depth, aggregate) => {
|
|
144
288
|
if (--remaining < 0 ||
|
|
@@ -149,6 +293,24 @@ function checkMeasures(input) {
|
|
|
149
293
|
report("invalid-measure", field, "Invalid or overly complex expression.");
|
|
150
294
|
return false;
|
|
151
295
|
}
|
|
296
|
+
const conditions = conditionsOf(expression);
|
|
297
|
+
const balance = balanceOf(expression);
|
|
298
|
+
if (conditions !== undefined && expression.op !== "filtered") {
|
|
299
|
+
if (isAggregate(expression)) {
|
|
300
|
+
checkConditions(conditions);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
report("invalid-measure", field, "Only count, sum, countDistinct, avg, min and max take where.");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (balance !== undefined) {
|
|
307
|
+
if (isAggregate(expression)) {
|
|
308
|
+
checkBalance(balance);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
report("invalid-measure", field, "Only count, sum, countDistinct, avg, min and max take balanceOver.");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
152
314
|
switch (expression.op) {
|
|
153
315
|
case "count":
|
|
154
316
|
if (aggregate !== undefined) {
|
|
@@ -171,9 +333,12 @@ function checkMeasures(input) {
|
|
|
171
333
|
"property.");
|
|
172
334
|
}
|
|
173
335
|
}
|
|
174
|
-
else if (aggregate === undefined
|
|
336
|
+
else if (aggregate === undefined) {
|
|
175
337
|
report("invalid-measure", field, "Sums require a numeric property.");
|
|
176
338
|
}
|
|
339
|
+
else if (!["integer", "number"].includes(type)) {
|
|
340
|
+
report("invalid-measure", field, "avg, min, max and sum require a numeric property.");
|
|
341
|
+
}
|
|
177
342
|
return false;
|
|
178
343
|
}
|
|
179
344
|
case "measure": {
|
|
@@ -194,6 +359,22 @@ function checkMeasures(input) {
|
|
|
194
359
|
}
|
|
195
360
|
return visit(target, [...references, expression.name], depth + 1);
|
|
196
361
|
}
|
|
362
|
+
case "filtered": {
|
|
363
|
+
// The wrapper counts as the aggregate it wraps, which must not declare conditions too.
|
|
364
|
+
const args = Array.isArray(expression.args)
|
|
365
|
+
? expression.args
|
|
366
|
+
: [];
|
|
367
|
+
const [inner] = args;
|
|
368
|
+
if (args.length !== 1 ||
|
|
369
|
+
!isRecord(inner) ||
|
|
370
|
+
!isAggregate(inner) ||
|
|
371
|
+
conditionsOf(inner) !== undefined) {
|
|
372
|
+
report("invalid-measure", field, "Invalid expression operator or arity.");
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
checkConditions(expression.where);
|
|
376
|
+
return visit(inner, references, depth + 1, aggregate);
|
|
377
|
+
}
|
|
197
378
|
default: {
|
|
198
379
|
const aggregates = AGGREGATES.has(expression.op);
|
|
199
380
|
if (!OPERATORS.has(expression.op) ||
|
|
@@ -221,6 +402,18 @@ function checkMeasures(input) {
|
|
|
221
402
|
if (measure.unit === "currency" && !hasCurrency) {
|
|
222
403
|
report("invalid-measure", field, "Currency measures require source.currency.");
|
|
223
404
|
}
|
|
405
|
+
if (balances.size > 1) {
|
|
406
|
+
report("invalid-measure", field, "A measure reads balances over two properties.");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// The platform builds a model of every warehouse-backed entity, and of each stored entity with
|
|
410
|
+
// measures. Measured only once the measures are valid, which writing them requires.
|
|
411
|
+
if (refusals === 0 && (!stored || measureCount > 0)) {
|
|
412
|
+
const bytes = new TextEncoder().encode(JSON.stringify(modelDefinition(entity))).byteLength;
|
|
413
|
+
if (bytes > MAXIMUM_MODEL_BYTES) {
|
|
414
|
+
input.report("invalid-measure", entity.name, `${entity.name}'s model, its dimensions and measures as SegmentStream reads them, takes ` +
|
|
415
|
+
`${(bytes / 1024).toFixed(1)} KiB; the limit is ${MAXIMUM_MODEL_BYTES / 1024} KiB.`);
|
|
416
|
+
}
|
|
224
417
|
}
|
|
225
418
|
}
|
|
226
419
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/ontology",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Define a SegmentStream workspace app's ontology: entities, relationships, roles and actions, written with Effect.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SegmentStream",
|