@segmentstream-cloud/ontology 0.12.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 +46 -16
- 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/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/model.d.ts +1 -1
- package/dist/replication/model.js +11 -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 +2 -1
- package/dist/service/handlers.js +66 -28
- package/dist/service/mcp/server.d.ts +3 -0
- package/dist/service/mcp/server.js +60 -8
- 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/schemas.d.ts +99 -1
- package/dist/service/schemas.js +150 -2
- package/dist/service/service.js +8 -2
- package/dist/user.d.ts +1 -1
- package/dist/warehouse-backed.js +222 -121
- package/package.json +1 -1
package/dist/warehouse-backed.js
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import { SNAKE_CASE_NAME } from "./names.js";
|
|
1
|
+
import { isSystemProperty, SNAKE_CASE_NAME } from "./names.js";
|
|
2
2
|
const columnName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
3
|
const types = new Set(["text", "integer", "number", "boolean", "date", "timestamp", "select"]);
|
|
4
|
+
/**
|
|
5
|
+
* Types a join key may have. Joins deduplicate their target with `PARTITION BY` the key, which
|
|
6
|
+
* BigQuery refuses for FLOAT64; booleans, JSON and files identify nothing.
|
|
7
|
+
*/
|
|
8
|
+
const joinTypes = new Set(["text", "email", "url", "select", "integer", "date", "timestamp"]);
|
|
9
|
+
/** Types `m.countDistinct` counts: the join key types, whose values compare exactly. */
|
|
10
|
+
const distinctTypes = joinTypes;
|
|
4
11
|
/** Cross-field validation runs after relationships have resolved their target entities. */
|
|
5
12
|
export function checkWarehouseSource(entities, report) {
|
|
6
13
|
for (const entity of entities) {
|
|
7
14
|
const { source, measures = {} } = entity.entity.definition;
|
|
8
15
|
const path = entity.name;
|
|
9
16
|
if (source === undefined) {
|
|
10
|
-
if (Object.keys(measures).length) {
|
|
11
|
-
report("invalid-measure", path, "
|
|
17
|
+
if (entity.builtIn && Object.keys(measures).length) {
|
|
18
|
+
report("invalid-measure", path, "User cannot declare measures.");
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
checkMeasures({
|
|
22
|
+
entity,
|
|
23
|
+
measures,
|
|
24
|
+
properties: new Map(entity.properties.map((property) => [property.name, property.definition])),
|
|
25
|
+
hasCurrency: false,
|
|
26
|
+
report,
|
|
27
|
+
});
|
|
12
28
|
}
|
|
13
29
|
if (entity.properties.some(({ definition }) => definition.column !== undefined || definition.scope !== undefined)) {
|
|
14
30
|
report("invalid-column", path, "Column mappings and scopes belong to warehouse-backed entities.");
|
|
@@ -39,11 +55,9 @@ export function checkWarehouseSource(entities, report) {
|
|
|
39
55
|
else if (!columnName.test(property.column)) {
|
|
40
56
|
report("invalid-column", field, "Invalid warehouse column.");
|
|
41
57
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
property
|
|
45
|
-
"default" in property) {
|
|
46
|
-
report("invalid-property", field, "Warehouse-backed properties must use a supported scalar type without unique, managed or default.");
|
|
58
|
+
// `unique` is allowed: an unenforced assertion that makes the property a join target.
|
|
59
|
+
if (!types.has(property.type) || property.managed || "default" in property) {
|
|
60
|
+
report("invalid-property", field, "Warehouse-backed properties must use a supported scalar type without managed or default.");
|
|
47
61
|
}
|
|
48
62
|
let remainingScopes = 10_000;
|
|
49
63
|
const visit = (key, ancestors) => {
|
|
@@ -71,127 +85,214 @@ export function checkWarehouseSource(entities, report) {
|
|
|
71
85
|
if (source?.currency !== undefined && !hasCurrency) {
|
|
72
86
|
report("invalid-source", path, "Currency must name a text property.");
|
|
73
87
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
checkMeasures({ entity, measures, properties, hasCurrency, report });
|
|
89
|
+
}
|
|
90
|
+
for (const resolved of entity.relationships) {
|
|
91
|
+
checkRelationship({ entity, resolved, entities, report });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The aggregates a measure expression may contain. */
|
|
96
|
+
const AGGREGATES = new Set(["sum", "countDistinct"]);
|
|
97
|
+
const OPERATORS = new Set([...AGGREGATES, "add", "subtract", "multiply", "divide"]);
|
|
98
|
+
/**
|
|
99
|
+
* The most dimensions and measures the platform accepts in one aggregate model. It also refuses a
|
|
100
|
+
* model definition over 60 KiB, which the SDK cannot measure exactly.
|
|
101
|
+
*/
|
|
102
|
+
const MAXIMUM_MODEL_MEMBERS = 100;
|
|
103
|
+
/** Stored property types that are not dimensions: their values cannot be grouped by. */
|
|
104
|
+
const UNGROUPABLE_TYPES = new Set(["json", "blob"]);
|
|
105
|
+
/**
|
|
106
|
+
* The measures of a warehouse-backed or stored entity. A measure's columns name properties of the
|
|
107
|
+
* entity. `hasCurrency` says whether the entity maps a text `source.currency`, which stored entities
|
|
108
|
+
* never do, so their measures cannot be currency amounts.
|
|
109
|
+
*/
|
|
110
|
+
function checkMeasures(input) {
|
|
111
|
+
const { entity, measures, properties, hasCurrency, report } = input;
|
|
112
|
+
// A stored entity's rows carry its system fields, whose names a measure cannot take.
|
|
113
|
+
const stored = entity.entity.definition.source === undefined;
|
|
114
|
+
const measureCount = Object.keys(measures).length;
|
|
115
|
+
// A stored entity's dimensions are its id and its groupable properties.
|
|
116
|
+
const dimensionCount = stored
|
|
117
|
+
? 1 + [...properties.values()].filter(({ type }) => !UNGROUPABLE_TYPES.has(type)).length
|
|
118
|
+
: properties.size;
|
|
119
|
+
if (measureCount > 0 && dimensionCount + measureCount > MAXIMUM_MODEL_MEMBERS) {
|
|
120
|
+
report("invalid-measure", `${entity.name}.measures`, `${entity.name} has ${dimensionCount + measureCount} dimensions and measures; an entity with ` +
|
|
121
|
+
`measures may have at most ${MAXIMUM_MODEL_MEMBERS}${stored ? ", counting its id" : ""}, ` +
|
|
122
|
+
"and its definition at most 60 KiB.");
|
|
123
|
+
}
|
|
124
|
+
for (const [name, measure] of Object.entries(measures)) {
|
|
125
|
+
const field = `${entity.name}.measures.${name}`;
|
|
126
|
+
if (!SNAKE_CASE_NAME.test(name) ||
|
|
127
|
+
properties.has(name) ||
|
|
128
|
+
(stored && isSystemProperty(name)) ||
|
|
129
|
+
entity.relationships.some((r) => r.name === name) ||
|
|
130
|
+
!measure.description?.trim()) {
|
|
131
|
+
report("invalid-measure", field, "Measures require unique snake_case names and a description.");
|
|
132
|
+
}
|
|
133
|
+
if (measure.unit !== undefined &&
|
|
134
|
+
!["number", "currency", "ratio", "percent"].includes(measure.unit)) {
|
|
135
|
+
report("invalid-measure", field, "Invalid measure unit.");
|
|
136
|
+
}
|
|
137
|
+
if (measure.format !== undefined &&
|
|
138
|
+
!["decimal", "currency", "percent"].includes(measure.format)) {
|
|
139
|
+
report("invalid-measure", field, "Invalid measure format.");
|
|
140
|
+
}
|
|
141
|
+
let remaining = 10_000;
|
|
142
|
+
/** `aggregate` is the aggregate the expression is an argument of, if any. */
|
|
143
|
+
const visit = (expression, references, depth, aggregate) => {
|
|
144
|
+
if (--remaining < 0 ||
|
|
145
|
+
depth > 30 ||
|
|
146
|
+
references.length > 20 ||
|
|
147
|
+
!expression ||
|
|
148
|
+
typeof expression !== "object") {
|
|
149
|
+
report("invalid-measure", field, "Invalid or overly complex expression.");
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
switch (expression.op) {
|
|
153
|
+
case "count":
|
|
154
|
+
if (aggregate !== undefined) {
|
|
155
|
+
report("invalid-measure", field, "Nested aggregates are unsupported.");
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
case "literal":
|
|
159
|
+
if (typeof expression.value !== "string" ||
|
|
160
|
+
expression.value.length >= 100 ||
|
|
161
|
+
!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/.test(expression.value) ||
|
|
162
|
+
!Number.isFinite(Number(expression.value))) {
|
|
163
|
+
report("invalid-measure", field, "Literals must be finite numbers.");
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
case "column": {
|
|
167
|
+
const type = properties.get(expression.name)?.type ?? "";
|
|
168
|
+
if (aggregate === "countDistinct") {
|
|
169
|
+
if (!distinctTypes.has(type)) {
|
|
170
|
+
report("invalid-measure", field, "Distinct counts require a text, email, url, select, integer, date or timestamp " +
|
|
171
|
+
"property.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (aggregate === undefined || !["integer", "number"].includes(type)) {
|
|
175
|
+
report("invalid-measure", field, "Sums require a numeric property.");
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
89
178
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
references.length > 20 ||
|
|
95
|
-
!expression ||
|
|
96
|
-
typeof expression !== "object") {
|
|
97
|
-
report("invalid-measure", field, "Invalid or overly complex expression.");
|
|
179
|
+
case "measure": {
|
|
180
|
+
const target = measures[expression.name];
|
|
181
|
+
if (!target) {
|
|
182
|
+
report("unknown-measure", field, "Unknown measure reference.");
|
|
98
183
|
return false;
|
|
99
184
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
report("invalid-measure", field, "Nested aggregates are unsupported.");
|
|
104
|
-
}
|
|
105
|
-
return true;
|
|
106
|
-
case "literal":
|
|
107
|
-
if (typeof expression.value !== "string" ||
|
|
108
|
-
expression.value.length >= 100 ||
|
|
109
|
-
!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/.test(expression.value) ||
|
|
110
|
-
!Number.isFinite(Number(expression.value))) {
|
|
111
|
-
report("invalid-measure", field, "Literals must be finite numbers.");
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
case "column":
|
|
115
|
-
if (!inside ||
|
|
116
|
-
!["integer", "number"].includes(properties.get(expression.name)?.type ?? "")) {
|
|
117
|
-
report("invalid-measure", field, "Sums require a numeric property.");
|
|
118
|
-
}
|
|
119
|
-
return false;
|
|
120
|
-
case "measure": {
|
|
121
|
-
const target = measures[expression.name];
|
|
122
|
-
if (!target) {
|
|
123
|
-
report("unknown-measure", field, "Unknown measure reference.");
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
if (references.includes(expression.name)) {
|
|
127
|
-
report("circular-measure", field, "Circular measure reference.");
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
if (inside) {
|
|
131
|
-
report("invalid-measure", field, "Measure references cannot be aggregated again.");
|
|
132
|
-
}
|
|
133
|
-
if (target.unit === "currency" && !hasCurrency) {
|
|
134
|
-
report("invalid-measure", field, "Currency measures require source.currency.");
|
|
135
|
-
}
|
|
136
|
-
return visit(target, [...references, expression.name], depth + 1);
|
|
137
|
-
}
|
|
138
|
-
default: {
|
|
139
|
-
if (!["sum", "add", "subtract", "multiply", "divide"].includes(expression.op) ||
|
|
140
|
-
!Array.isArray(expression.args) ||
|
|
141
|
-
expression.args.length !== (expression.op === "sum" ? 1 : 2)) {
|
|
142
|
-
report("invalid-measure", field, "Invalid expression operator or arity.");
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
if (inside && expression.op === "sum") {
|
|
146
|
-
report("invalid-measure", field, "Nested aggregates are unsupported.");
|
|
147
|
-
}
|
|
148
|
-
const aggregated = expression.args
|
|
149
|
-
.map((arg) => visit(arg, references, depth + 1, inside || expression.op === "sum"))
|
|
150
|
-
.some(Boolean);
|
|
151
|
-
return expression.op === "sum" || aggregated;
|
|
152
|
-
}
|
|
185
|
+
if (references.includes(expression.name)) {
|
|
186
|
+
report("circular-measure", field, "Circular measure reference.");
|
|
187
|
+
return false;
|
|
153
188
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
if (aggregate !== undefined) {
|
|
190
|
+
report("invalid-measure", field, "Measure references cannot be aggregated again.");
|
|
191
|
+
}
|
|
192
|
+
if (target.unit === "currency" && !hasCurrency) {
|
|
193
|
+
report("invalid-measure", field, "Currency measures require source.currency.");
|
|
194
|
+
}
|
|
195
|
+
return visit(target, [...references, expression.name], depth + 1);
|
|
157
196
|
}
|
|
158
|
-
|
|
159
|
-
|
|
197
|
+
default: {
|
|
198
|
+
const aggregates = AGGREGATES.has(expression.op);
|
|
199
|
+
if (!OPERATORS.has(expression.op) ||
|
|
200
|
+
!Array.isArray(expression.args) ||
|
|
201
|
+
expression.args.length !== (aggregates ? 1 : 2)) {
|
|
202
|
+
report("invalid-measure", field, "Invalid expression operator or arity.");
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (aggregates && aggregate !== undefined) {
|
|
206
|
+
report("invalid-measure", field, "Nested aggregates are unsupported.");
|
|
207
|
+
}
|
|
208
|
+
if (expression.op === "countDistinct" && expression.args[0]?.op !== "column") {
|
|
209
|
+
report("invalid-measure", field, "Distinct counts take one property.");
|
|
210
|
+
}
|
|
211
|
+
const aggregated = expression.args
|
|
212
|
+
.map((arg) => visit(arg, references, depth + 1, aggregates ? expression.op : aggregate))
|
|
213
|
+
.some(Boolean);
|
|
214
|
+
return aggregates || aggregated;
|
|
160
215
|
}
|
|
161
216
|
}
|
|
217
|
+
};
|
|
218
|
+
if (!visit(measure, [name], 0)) {
|
|
219
|
+
report("invalid-measure", field, "A measure must contain an aggregate.");
|
|
162
220
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const field = `${path}.relationships.${name}`;
|
|
166
|
-
if (landing?.entity.definition.source !== undefined) {
|
|
167
|
-
report("invalid-join", field, "Relationships cannot target warehouse-backed entities in this release.");
|
|
168
|
-
}
|
|
169
|
-
if (source === undefined) {
|
|
170
|
-
if (relationship.cardinality === "one" && relationship.on !== undefined) {
|
|
171
|
-
report("invalid-join", field, "Stored relationships cannot declare on.");
|
|
172
|
-
}
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (relationship.cardinality !== "one") {
|
|
176
|
-
report("invalid-join", field, "Warehouse-backed entities support rel.one only.");
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const pairs = Object.entries(relationship.on ?? {});
|
|
180
|
-
if (pairs.length !== 1) {
|
|
181
|
-
report(pairs.length ? "invalid-join" : "missing-join", field, "Declare one property pair in on.");
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const [own, other] = pairs[0];
|
|
185
|
-
const ownType = entity.properties.find((p) => p.name === own)?.definition.type;
|
|
186
|
-
const targetProperty = landing?.properties.find((p) => p.name === other)?.definition;
|
|
187
|
-
const targetType = other === "id" ? "text" : targetProperty?.type;
|
|
188
|
-
const text = (type) => ["email", "url", "select"].includes(type ?? "") ? "text" : type;
|
|
189
|
-
if (!ownType ||
|
|
190
|
-
!targetType ||
|
|
191
|
-
text(ownType) !== text(targetType) ||
|
|
192
|
-
(other !== "id" && !targetProperty?.unique)) {
|
|
193
|
-
report("invalid-join", field, "Join properties must have compatible types and target id or a unique property.");
|
|
194
|
-
}
|
|
221
|
+
if (measure.unit === "currency" && !hasCurrency) {
|
|
222
|
+
report("invalid-measure", field, "Currency measures require source.currency.");
|
|
195
223
|
}
|
|
196
224
|
}
|
|
197
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* One case per pairing. Stored → stored relationships never declare `on`. A relationship with a
|
|
228
|
+
* warehouse-backed end is value-matched: a `rel.one` with one `on` pair and no delete rule or
|
|
229
|
+
* optionality, because no key column or foreign key backs it.
|
|
230
|
+
*/
|
|
231
|
+
function checkRelationship(input) {
|
|
232
|
+
const { entity, resolved, entities, report } = input;
|
|
233
|
+
const { name, relationship, target } = resolved;
|
|
234
|
+
const landing = entities.find((candidate) => candidate.name === target);
|
|
235
|
+
const field = `${entity.name}.relationships.${name}`;
|
|
236
|
+
const storedOwner = entity.entity.definition.source === undefined;
|
|
237
|
+
const targetSource = landing?.entity.definition.source;
|
|
238
|
+
if (storedOwner && targetSource === undefined) {
|
|
239
|
+
if (relationship.cardinality === "one" && relationship.on !== undefined) {
|
|
240
|
+
report("invalid-join", field, "Stored relationships cannot declare on.");
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (relationship.cardinality !== "one") {
|
|
245
|
+
const message = storedOwner
|
|
246
|
+
? "Relationships to warehouse-backed entities support rel.one only."
|
|
247
|
+
: "Warehouse-backed entities support rel.one only.";
|
|
248
|
+
report("invalid-join", field, message);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const pairs = Object.entries(relationship.on ?? {});
|
|
252
|
+
const [own, other] = pairs.length === 1 ? pairs[0] : [undefined, undefined];
|
|
253
|
+
if (relationship.onDelete !== undefined || relationship.optional) {
|
|
254
|
+
const message = storedOwner
|
|
255
|
+
? "A relationship to a warehouse-backed entity has no delete rule; its optionality follows " +
|
|
256
|
+
`${own ?? "the property named in on"}.`
|
|
257
|
+
: "Relationships of warehouse-backed entities have no delete rule or optionality: their " +
|
|
258
|
+
"joins never drop a row.";
|
|
259
|
+
report("invalid-join", field, message);
|
|
260
|
+
}
|
|
261
|
+
if (own === undefined || other === undefined) {
|
|
262
|
+
report(pairs.length ? "invalid-join" : "missing-join", field, "Declare one property pair in on.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const ownProperty = entity.properties.find((property) => property.name === own)?.definition;
|
|
266
|
+
if (storedOwner && ownProperty === undefined) {
|
|
267
|
+
report("invalid-join", field, `The own side of on must be a declared property of ${entity.name}: it holds the reference.`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const targetProperty = landing?.properties.find((property) => property.name === other);
|
|
271
|
+
const text = (type) => ["email", "url", "select"].includes(type ?? "") ? "text" : type;
|
|
272
|
+
let targetType;
|
|
273
|
+
let keyed;
|
|
274
|
+
if (targetSource === undefined) {
|
|
275
|
+
targetType = other === "id" ? "text" : targetProperty?.definition.type;
|
|
276
|
+
keyed = other === "id" || targetProperty?.definition.unique === true;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const { key } = targetSource;
|
|
280
|
+
const singleKey = Array.isArray(key) && key.length === 1 ? key[0] : undefined;
|
|
281
|
+
targetType = targetProperty?.definition.type;
|
|
282
|
+
keyed =
|
|
283
|
+
targetProperty !== undefined &&
|
|
284
|
+
(targetProperty.definition.unique === true ||
|
|
285
|
+
(singleKey !== undefined && targetProperty.definition.column === singleKey));
|
|
286
|
+
}
|
|
287
|
+
if ((ownProperty !== undefined && !joinTypes.has(ownProperty.type)) ||
|
|
288
|
+
(targetType !== undefined && !joinTypes.has(targetType))) {
|
|
289
|
+
report("invalid-join", field, "Join properties must be text, integer, date or timestamp.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!ownProperty || !targetType || text(ownProperty.type) !== text(targetType) || !keyed) {
|
|
293
|
+
const accepted = targetSource === undefined
|
|
294
|
+
? "id or a unique property"
|
|
295
|
+
: "the property that maps its single source key column, or a unique property";
|
|
296
|
+
report("invalid-join", field, `Join properties must have compatible types and target ${accepted}.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segmentstream-cloud/ontology",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|