@nexia/sdk 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-availability.d.ts +18 -0
- package/dist/app-availability.js +29 -0
- package/dist/approval.d.ts +180 -0
- package/dist/approval.js +9 -0
- package/dist/context.d.ts +49 -0
- package/dist/context.js +1 -0
- package/dist/data-migration.d.ts +123 -0
- package/dist/data-migration.js +70 -0
- package/dist/fixtures/approval-contract-negative.d.ts +1 -0
- package/dist/fixtures/approval-contract-negative.js +8 -0
- package/dist/handoff-result.d.ts +6 -0
- package/dist/handoff-result.js +17 -0
- package/dist/host.d.ts +602 -0
- package/dist/host.js +803 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +21 -0
- package/dist/number.d.ts +2 -0
- package/dist/number.js +9 -0
- package/dist/organization-target.d.ts +101 -0
- package/dist/organization-target.js +105 -0
- package/dist/permissions.d.ts +3 -0
- package/dist/permissions.js +12 -0
- package/dist/platform.d.ts +378 -0
- package/dist/platform.js +114 -0
- package/dist/process.d.ts +98 -0
- package/dist/process.js +10 -0
- package/dist/resource-composition.d.ts +104 -0
- package/dist/resource-composition.js +576 -0
- package/dist/resource-create-destination.d.ts +17 -0
- package/dist/resource-create-destination.js +1 -0
- package/dist/resource-reference.d.ts +122 -0
- package/dist/resource-reference.js +203 -0
- package/dist/resources/resource-information.d.ts +128 -0
- package/dist/resources/resource-information.js +12 -0
- package/dist/resources/resource-list.d.ts +23 -0
- package/dist/resources/resource-list.js +1 -0
- package/dist/resources/resource-projections.d.ts +311 -0
- package/dist/resources/resource-projections.js +1 -0
- package/dist/resources/resource-transfer.d.ts +208 -0
- package/dist/resources/resource-transfer.js +1 -0
- package/dist/shell.d.ts +1297 -0
- package/dist/shell.js +1 -0
- package/dist/signature.d.ts +489 -0
- package/dist/signature.js +171 -0
- package/dist/spreadsheet.d.ts +42 -0
- package/dist/spreadsheet.js +15 -0
- package/dist/testing.d.ts +16 -0
- package/dist/testing.js +17 -0
- package/package.json +53 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
export class CompositionSpecValidationError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
path;
|
|
4
|
+
constructor(code, path, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.path = path;
|
|
8
|
+
this.name = 'CompositionSpecValidationError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parses the stable wire shape. Resource, field, operator, relationship, grain,
|
|
13
|
+
* and authorization semantics remain live host-catalog decisions.
|
|
14
|
+
*/
|
|
15
|
+
export function parseCompositionSpec(value) {
|
|
16
|
+
const candidate = plainObject(value, '$');
|
|
17
|
+
const schemaVersion = candidate.schema_version;
|
|
18
|
+
if (schemaVersion !== 1 && schemaVersion !== 2 && schemaVersion !== 3 && schemaVersion !== 4) {
|
|
19
|
+
fail('unsupported_schema_version', 'schema_version', 'Resource Composition schema_version must be 1, 2, 3, or 4.');
|
|
20
|
+
}
|
|
21
|
+
if (schemaVersion === 4)
|
|
22
|
+
return parseV4(candidate);
|
|
23
|
+
const root = object(candidate, '$', schemaVersion === 1 ? [
|
|
24
|
+
'schema_version',
|
|
25
|
+
'sources',
|
|
26
|
+
'relationship',
|
|
27
|
+
'fields',
|
|
28
|
+
'filters',
|
|
29
|
+
'dimensions',
|
|
30
|
+
'measures',
|
|
31
|
+
'result',
|
|
32
|
+
] : [
|
|
33
|
+
'schema_version',
|
|
34
|
+
'sources',
|
|
35
|
+
'relationships',
|
|
36
|
+
'fields',
|
|
37
|
+
'filters',
|
|
38
|
+
'dimensions',
|
|
39
|
+
'measures',
|
|
40
|
+
'result',
|
|
41
|
+
]);
|
|
42
|
+
const sourceValues = list(root.sources, 'sources');
|
|
43
|
+
if ((schemaVersion === 1 && (sourceValues.length < 1 || sourceValues.length > 2))
|
|
44
|
+
|| (schemaVersion === 2 && sourceValues.length !== 3)
|
|
45
|
+
|| (schemaVersion === 3 && sourceValues.length < 2)) {
|
|
46
|
+
fail('invalid_source_count', 'sources', schemaVersion === 1
|
|
47
|
+
? 'Resource Composition schema v1 requires one or two sources.'
|
|
48
|
+
: (schemaVersion === 2
|
|
49
|
+
? 'Resource Composition schema v2 requires exactly three sources.'
|
|
50
|
+
: 'Resource Composition schema v3 requires at least two sources.'));
|
|
51
|
+
}
|
|
52
|
+
if (schemaVersion === 1 && sourceValues.length === 2 && root.relationship === null) {
|
|
53
|
+
fail('relationship_required', 'relationship', 'Two-source Resource Composition requires one relationship.');
|
|
54
|
+
}
|
|
55
|
+
if (schemaVersion === 1 && sourceValues.length === 1 && root.relationship !== null) {
|
|
56
|
+
fail('relationship_not_allowed', 'relationship', 'Single-source Resource Composition cannot declare a relationship.');
|
|
57
|
+
}
|
|
58
|
+
const sourceAliases = new Set();
|
|
59
|
+
const resourceKeys = new Set();
|
|
60
|
+
const sources = sourceValues.map((value, index) => {
|
|
61
|
+
const path = `sources.${index}`;
|
|
62
|
+
const source = object(value, path, ['alias', 'resource_key']);
|
|
63
|
+
const alias = identifier(source.alias, `${path}.alias`, ALIAS_PATTERN);
|
|
64
|
+
const resourceKey = identifier(source.resource_key, `${path}.resource_key`, RESOURCE_KEY_PATTERN);
|
|
65
|
+
if (sourceAliases.has(alias) || (schemaVersion !== 3 && resourceKeys.has(resourceKey))) {
|
|
66
|
+
fail('duplicate_identifier', path, schemaVersion === 3
|
|
67
|
+
? 'Resource Composition source aliases must be unique.'
|
|
68
|
+
: 'Resource Composition source aliases and Resource keys must be unique.');
|
|
69
|
+
}
|
|
70
|
+
sourceAliases.add(alias);
|
|
71
|
+
resourceKeys.add(resourceKey);
|
|
72
|
+
return { alias, resource_key: resourceKey };
|
|
73
|
+
});
|
|
74
|
+
const aliases = sources.map(({ alias }) => alias);
|
|
75
|
+
const fields = parseSelections(root.fields, 'fields', aliases);
|
|
76
|
+
const filters = list(root.filters, 'filters').map((value, index) => {
|
|
77
|
+
const path = `filters.${index}`;
|
|
78
|
+
const filter = object(value, path, ['source', 'field', 'operator', 'value']);
|
|
79
|
+
const operator = oneOf(filter.operator, `${path}.operator`, ['eq', 'in'], 'invalid_filter_value');
|
|
80
|
+
const filterValue = operator === 'in'
|
|
81
|
+
? scalarList(filter.value, `${path}.value`)
|
|
82
|
+
: scalar(filter.value, `${path}.value`);
|
|
83
|
+
return {
|
|
84
|
+
source: source(filter.source, aliases, `${path}.source`),
|
|
85
|
+
field: identifier(filter.field, `${path}.field`, FIELD_PATTERN),
|
|
86
|
+
operator,
|
|
87
|
+
value: filterValue,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
const dimensions = [];
|
|
91
|
+
const dimensionKeys = new Set();
|
|
92
|
+
for (const [index, value] of list(root.dimensions, 'dimensions').entries()) {
|
|
93
|
+
const path = `dimensions.${index}`;
|
|
94
|
+
const dimension = object(value, path, ['source', 'field', 'bucket']);
|
|
95
|
+
const selectedSource = source(dimension.source, aliases, `${path}.source`);
|
|
96
|
+
const field = identifier(dimension.field, `${path}.field`, FIELD_PATTERN);
|
|
97
|
+
const bucket = dimension.bucket === null
|
|
98
|
+
? null
|
|
99
|
+
: oneOf(dimension.bucket, `${path}.bucket`, ['day', 'week', 'month']);
|
|
100
|
+
const key = `${selectedSource}.${field}`;
|
|
101
|
+
if (dimensionKeys.has(key))
|
|
102
|
+
fail('duplicate_identifier', path, 'Resource Composition entries must be unique.');
|
|
103
|
+
dimensionKeys.add(key);
|
|
104
|
+
dimensions.push({ source: selectedSource, field, bucket });
|
|
105
|
+
}
|
|
106
|
+
const measures = [];
|
|
107
|
+
const measureAliases = new Set();
|
|
108
|
+
for (const [index, value] of list(root.measures, 'measures').entries()) {
|
|
109
|
+
const measure = parseMeasure(value, index, aliases, measureAliases);
|
|
110
|
+
measures.push(measure);
|
|
111
|
+
}
|
|
112
|
+
const resultValue = object(root.result, 'result', ['shape']);
|
|
113
|
+
const result = { shape: oneOf(resultValue.shape, 'result.shape', ['rows', 'aggregate']) };
|
|
114
|
+
assertResultShape(result.shape, fields, dimensions, measures);
|
|
115
|
+
const body = {
|
|
116
|
+
sources,
|
|
117
|
+
fields,
|
|
118
|
+
filters,
|
|
119
|
+
dimensions,
|
|
120
|
+
measures,
|
|
121
|
+
result,
|
|
122
|
+
};
|
|
123
|
+
if (schemaVersion === 1) {
|
|
124
|
+
return { schema_version: 1, ...body, relationship: parseRelationship(root.relationship) };
|
|
125
|
+
}
|
|
126
|
+
if (schemaVersion === 2) {
|
|
127
|
+
return { schema_version: 2, ...body, relationships: parseRelationships(root.relationships) };
|
|
128
|
+
}
|
|
129
|
+
return { schema_version: 3, ...body, relationships: parseGraphRelationships(root.relationships, aliases) };
|
|
130
|
+
}
|
|
131
|
+
function parseV4(candidate) {
|
|
132
|
+
const root = object(candidate, '$', ['schema_version', 'sources', 'relationships', 'fields', 'timezone', 'where', 'dimensions', 'measures', 'having', 'derived', 'comparison', 'sort', 'limit', 'result']);
|
|
133
|
+
let nodes = 0;
|
|
134
|
+
const boundedList = (value, path) => {
|
|
135
|
+
const values = list(value, path);
|
|
136
|
+
if (values.length > 100)
|
|
137
|
+
fail('invalid_shape', path, 'Resource Composition list is too large.');
|
|
138
|
+
return values;
|
|
139
|
+
};
|
|
140
|
+
const node = (path, depth = 0) => {
|
|
141
|
+
if (depth > 8 || ++nodes > 100)
|
|
142
|
+
fail('invalid_shape', path, 'Resource Composition is too complex.');
|
|
143
|
+
};
|
|
144
|
+
const scalarV4 = (value, path) => {
|
|
145
|
+
if ((typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') || (typeof value === 'number' && !Number.isFinite(value)) || (typeof value === 'string' && value.length > 512))
|
|
146
|
+
fail('invalid_filter_value', path, 'Resource Composition scalar is invalid.');
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
const target = (value, path, allowDerived = true) => {
|
|
150
|
+
const item = plainObject(value, path);
|
|
151
|
+
if (item.kind === 'field') {
|
|
152
|
+
object(item, path, ['kind', 'source', 'field']);
|
|
153
|
+
return { kind: 'field', source: source(item.source, aliases, `${path}.source`), field: identifier(item.field, `${path}.field`, FIELD_PATTERN) };
|
|
154
|
+
}
|
|
155
|
+
if (allowDerived && item.kind === 'derived') {
|
|
156
|
+
object(item, path, ['kind', 'alias']);
|
|
157
|
+
return { kind: 'derived', alias: identifier(item.alias, `${path}.alias`, ALIAS_PATTERN) };
|
|
158
|
+
}
|
|
159
|
+
fail('invalid_shape', `${path}.kind`, 'Resource Composition target is invalid.');
|
|
160
|
+
};
|
|
161
|
+
const predicate = (value, path, depth = 0, aggregate = false) => {
|
|
162
|
+
node(path, depth);
|
|
163
|
+
const item = plainObject(value, path);
|
|
164
|
+
const kind = item.kind;
|
|
165
|
+
if (kind === 'all' || kind === 'any') {
|
|
166
|
+
object(item, path, ['kind', 'children']);
|
|
167
|
+
return { kind, children: boundedList(item.children, `${path}.children`).map((child, index) => predicate(child, `${path}.children.${index}`, depth + 1, aggregate)) };
|
|
168
|
+
}
|
|
169
|
+
if (kind === 'not') {
|
|
170
|
+
object(item, path, ['kind', 'child']);
|
|
171
|
+
return { kind, child: predicate(item.child, `${path}.child`, depth + 1, aggregate) };
|
|
172
|
+
}
|
|
173
|
+
if (kind === 'relative_date') {
|
|
174
|
+
if (aggregate)
|
|
175
|
+
fail('invalid_shape', path, 'Aggregate predicates cannot use relative dates.');
|
|
176
|
+
object(item, path, ['kind', 'target', 'period']);
|
|
177
|
+
return { kind, target: target(item.target, `${path}.target`), period: oneOf(item.period, `${path}.period`, ['today', 'yesterday', 'this_week', 'last_week', 'this_month', 'last_month', 'this_quarter', 'last_quarter', 'this_year', 'last_year'], 'invalid_filter_value') };
|
|
178
|
+
}
|
|
179
|
+
if (kind !== 'condition')
|
|
180
|
+
fail('invalid_shape', `${path}.kind`, 'Resource Composition predicate is invalid.');
|
|
181
|
+
object(item, path, ['kind', 'target', 'operator', 'value']);
|
|
182
|
+
const operator = oneOf(item.operator, `${path}.operator`, ['eq', 'in', 'neq', 'not_in', 'gt', 'gte', 'lt', 'lte', 'between', 'is_null', 'not_null', 'contains', 'not_contains', 'starts_with', 'ends_with'], 'invalid_filter_value');
|
|
183
|
+
let predicateValue = item.value;
|
|
184
|
+
if (operator === 'in' || operator === 'not_in') {
|
|
185
|
+
const values = boundedList(item.value, `${path}.value`);
|
|
186
|
+
if (values.length === 0)
|
|
187
|
+
fail('invalid_filter_value', `${path}.value`, 'The in filter requires a non-empty scalar list.');
|
|
188
|
+
predicateValue = values.map((entry) => scalarV4(entry, `${path}.value`));
|
|
189
|
+
}
|
|
190
|
+
else if (operator === 'between') {
|
|
191
|
+
const bounds = object(item.value, `${path}.value`, ['from', 'to']);
|
|
192
|
+
scalarV4(bounds.from, `${path}.value.from`);
|
|
193
|
+
scalarV4(bounds.to, `${path}.value.to`);
|
|
194
|
+
}
|
|
195
|
+
else if (operator === 'is_null' || operator === 'not_null') {
|
|
196
|
+
if (item.value !== null)
|
|
197
|
+
fail('invalid_filter_value', `${path}.value`, 'Null predicate requires null.');
|
|
198
|
+
}
|
|
199
|
+
else
|
|
200
|
+
scalarV4(item.value, `${path}.value`);
|
|
201
|
+
if (!aggregate)
|
|
202
|
+
return { kind, target: target(item.target, `${path}.target`), operator, value: predicateValue };
|
|
203
|
+
const aggregateTarget = object(item.target, `${path}.target`, ['kind', 'alias']);
|
|
204
|
+
return { kind, target: { kind: oneOf(aggregateTarget.kind, `${path}.target.kind`, ['measure', 'derived']), alias: identifier(aggregateTarget.alias, `${path}.target.alias`, ALIAS_PATTERN) }, operator, value: predicateValue };
|
|
205
|
+
};
|
|
206
|
+
const sources = boundedList(root.sources, 'sources').map((value, index) => {
|
|
207
|
+
const item = object(value, `sources.${index}`, ['alias', 'resource_key']);
|
|
208
|
+
return { alias: identifier(item.alias, `sources.${index}.alias`, ALIAS_PATTERN), resource_key: identifier(item.resource_key, `sources.${index}.resource_key`, RESOURCE_KEY_PATTERN) };
|
|
209
|
+
});
|
|
210
|
+
if (sources.length === 0)
|
|
211
|
+
fail('invalid_source_count', 'sources', 'Resource Composition schema v4 requires at least one source.');
|
|
212
|
+
const aliases = sources.map((source) => source.alias);
|
|
213
|
+
if (new Set(aliases).size !== aliases.length)
|
|
214
|
+
fail('duplicate_identifier', 'sources', 'Resource Composition source aliases must be unique.');
|
|
215
|
+
const relationships = boundedList(root.relationships, 'relationships');
|
|
216
|
+
if (sources.length === 1 && relationships.length !== 0)
|
|
217
|
+
fail('relationship_not_allowed', 'relationships', 'Single-source Resource Composition cannot declare relationships.');
|
|
218
|
+
const parsedRelationships = sources.length === 1 ? [] : parseGraphRelationships(relationships, aliases);
|
|
219
|
+
const timezone = identifier(root.timezone, 'timezone', /^[A-Za-z_]+(?:\/[A-Za-z_+-]+)+$/);
|
|
220
|
+
const fields = boundedList(root.fields, 'fields').map((value, index) => {
|
|
221
|
+
node(`fields.${index}`);
|
|
222
|
+
const item = plainObject(value, `fields.${index}`);
|
|
223
|
+
if ('derived' in item) {
|
|
224
|
+
object(item, `fields.${index}`, ['derived']);
|
|
225
|
+
return { derived: identifier(item.derived, `fields.${index}.derived`, ALIAS_PATTERN) };
|
|
226
|
+
}
|
|
227
|
+
object(item, `fields.${index}`, ['source', 'field']);
|
|
228
|
+
return { source: source(item.source, aliases, `fields.${index}.source`), field: identifier(item.field, `fields.${index}.field`, FIELD_PATTERN) };
|
|
229
|
+
});
|
|
230
|
+
const where = predicate(root.where, 'where');
|
|
231
|
+
const dimensions = boundedList(root.dimensions, 'dimensions').map((value, index) => { node(`dimensions.${index}`); const item = object(value, `dimensions.${index}`, ['target', 'bucket']); return { target: target(item.target, `dimensions.${index}.target`), bucket: item.bucket === null ? null : oneOf(item.bucket, `dimensions.${index}.bucket`, ['day', 'week', 'month', 'quarter', 'year']) }; });
|
|
232
|
+
const measures = boundedList(root.measures, 'measures').map((value, index) => {
|
|
233
|
+
node(`measures.${index}`);
|
|
234
|
+
const path = `measures.${index}`;
|
|
235
|
+
const item = object(value, path, ['alias', 'source', 'field', 'operation', 'result_type', 'null_behavior', 'unit', 'where']);
|
|
236
|
+
const operation = oneOf(item.operation, `${path}.operation`, ['count', 'distinct_count', 'sum', 'avg', 'min', 'max', 'median'], 'invalid_measure');
|
|
237
|
+
const result_type = oneOf(item.result_type, `${path}.result_type`, ['integer', 'decimal'], 'invalid_measure');
|
|
238
|
+
const null_behavior = oneOf(item.null_behavior, `${path}.null_behavior`, ['zero', 'null_if_empty'], 'invalid_measure');
|
|
239
|
+
const unit = identifier(item.unit, `${path}.unit`, UNIT_PATTERN, 'invalid_measure');
|
|
240
|
+
if (operation === 'count' && item.field !== null) {
|
|
241
|
+
fail('invalid_measure', `${path}.field`, 'Count cannot have a field.');
|
|
242
|
+
}
|
|
243
|
+
if ((operation === 'count' || operation === 'distinct_count')
|
|
244
|
+
&& (result_type !== 'integer' || null_behavior !== 'zero' || unit !== 'count')) {
|
|
245
|
+
fail('invalid_measure', path, 'Count measures require integer result, zero null behavior, and count unit.');
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
alias: identifier(item.alias, `${path}.alias`, ALIAS_PATTERN),
|
|
249
|
+
source: source(item.source, aliases, `${path}.source`),
|
|
250
|
+
field: operation === 'count' ? null : target(item.field, `${path}.field`),
|
|
251
|
+
operation,
|
|
252
|
+
result_type,
|
|
253
|
+
null_behavior,
|
|
254
|
+
unit,
|
|
255
|
+
where: predicate(item.where, `${path}.where`),
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
if (new Set(measures.map((measure) => measure.alias)).size !== measures.length)
|
|
259
|
+
fail('duplicate_identifier', 'measures', 'Measure aliases must be unique.');
|
|
260
|
+
const measureAliases = new Set(measures.map((measure) => measure.alias));
|
|
261
|
+
const derived = boundedList(root.derived, 'derived').map((value, index) => { node(`derived.${index}`); const path = `derived.${index}`; const item = plainObject(value, path); const stage = oneOf(item.stage, `${path}.stage`, ['row', 'aggregate']); const required = stage === 'row' ? ['alias', 'stage', 'source', 'result_type', 'unit', 'null_behavior', 'expression'] : ['alias', 'stage', 'result_type', 'unit', 'expression']; object(item, path, required); return { ...item, alias: identifier(item.alias, `${path}.alias`, ALIAS_PATTERN), stage, ...(stage === 'row' ? { source: source(item.source, aliases, `${path}.source`) } : {}), expression: item.expression }; });
|
|
262
|
+
if (new Set(derived.map((entry) => entry.alias)).size !== derived.length)
|
|
263
|
+
fail('duplicate_identifier', 'derived', 'Derived aliases must be unique.');
|
|
264
|
+
const stages = new Map(derived.map((entry) => [entry.alias, entry.stage]));
|
|
265
|
+
dimensions.forEach((dimension, index) => { if (dimension.target.kind === 'derived' && stages.get(dimension.target.alias) !== 'row')
|
|
266
|
+
fail('invalid_identifier', `dimensions.${index}.target.alias`, 'Row derived dimension is unavailable.'); });
|
|
267
|
+
measures.forEach((measure, index) => { if (measure.field?.kind === 'derived' && stages.get(measure.field.alias) !== 'row')
|
|
268
|
+
fail('invalid_identifier', `measures.${index}.field.alias`, 'Row derived measure field is unavailable.'); });
|
|
269
|
+
const expression = (value, path, stage, depth = 0) => {
|
|
270
|
+
node(path, depth);
|
|
271
|
+
const item = plainObject(value, path);
|
|
272
|
+
const allowed = stage === 'row'
|
|
273
|
+
? ['field', 'derived', 'literal', 'coalesce', 'date_diff_days', 'case', 'add', 'subtract', 'multiply', 'divide']
|
|
274
|
+
: ['measure', 'derived', 'literal', 'add', 'subtract', 'multiply', 'divide'];
|
|
275
|
+
const kind = oneOf(item.kind, `${path}.kind`, allowed);
|
|
276
|
+
if (kind === 'field') {
|
|
277
|
+
object(item, path, ['kind', 'source', 'field']);
|
|
278
|
+
return { kind, source: source(item.source, aliases, `${path}.source`), field: identifier(item.field, `${path}.field`, FIELD_PATTERN) };
|
|
279
|
+
}
|
|
280
|
+
if (kind === 'measure' || kind === 'derived') {
|
|
281
|
+
object(item, path, ['kind', 'alias']);
|
|
282
|
+
const alias = identifier(item.alias, `${path}.alias`, ALIAS_PATTERN);
|
|
283
|
+
if (kind === 'measure' && !measureAliases.has(alias))
|
|
284
|
+
fail('invalid_identifier', `${path}.alias`, 'Measure is unavailable.');
|
|
285
|
+
if (kind === 'derived' && stages.get(alias) !== stage)
|
|
286
|
+
fail('invalid_identifier', `${path}.alias`, 'Derived reference is unavailable.');
|
|
287
|
+
return { kind, alias };
|
|
288
|
+
}
|
|
289
|
+
if (kind === 'literal') {
|
|
290
|
+
object(item, path, ['kind', 'literal_type', 'value']);
|
|
291
|
+
const literalType = oneOf(item.literal_type, `${path}.literal_type`, ['number', 'string', 'boolean', 'null']);
|
|
292
|
+
const literal = item.value;
|
|
293
|
+
const invalidLiteral = (literalType === 'number' && (typeof literal !== 'number' || !Number.isFinite(literal)))
|
|
294
|
+
|| (literalType === 'string' && (typeof literal !== 'string' || literal.length > 512))
|
|
295
|
+
|| (literalType === 'boolean' && typeof literal !== 'boolean')
|
|
296
|
+
|| (literalType === 'null' && literal !== null);
|
|
297
|
+
if (invalidLiteral)
|
|
298
|
+
fail('invalid_shape', `${path}.value`, 'Literal type does not match value.');
|
|
299
|
+
return { kind, literal_type: literalType, value: literal };
|
|
300
|
+
}
|
|
301
|
+
if (kind === 'coalesce') {
|
|
302
|
+
object(item, path, ['kind', 'values']);
|
|
303
|
+
return { kind, values: boundedList(item.values, `${path}.values`).map((child, index) => expression(child, `${path}.values.${index}`, stage, depth + 1)) };
|
|
304
|
+
}
|
|
305
|
+
if (kind === 'case') {
|
|
306
|
+
object(item, path, ['kind', 'branches', 'otherwise']);
|
|
307
|
+
return {
|
|
308
|
+
kind,
|
|
309
|
+
branches: boundedList(item.branches, `${path}.branches`).map((branch, index) => {
|
|
310
|
+
const branchPath = `${path}.branches.${index}`;
|
|
311
|
+
const parsedBranch = object(branch, branchPath, ['when', 'then']);
|
|
312
|
+
return { when: predicate(parsedBranch.when, `${branchPath}.when`, depth + 1), then: expression(parsedBranch.then, `${branchPath}.then`, stage, depth + 1) };
|
|
313
|
+
}),
|
|
314
|
+
otherwise: expression(item.otherwise, `${path}.otherwise`, stage, depth + 1),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (kind === 'date_diff_days') {
|
|
318
|
+
object(item, path, ['kind', 'start', 'end']);
|
|
319
|
+
return { kind, start: expression(item.start, `${path}.start`, stage, depth + 1), end: expression(item.end, `${path}.end`, stage, depth + 1) };
|
|
320
|
+
}
|
|
321
|
+
object(item, path, ['kind', 'left', 'right']);
|
|
322
|
+
return { kind, left: expression(item.left, `${path}.left`, stage, depth + 1), right: expression(item.right, `${path}.right`, stage, depth + 1) };
|
|
323
|
+
};
|
|
324
|
+
derived.forEach((entry, index) => { entry.expression = expression(entry.expression, `derived.${index}.expression`, entry.stage); });
|
|
325
|
+
const dependencies = new Map(derived.map((entry) => [entry.alias, []]));
|
|
326
|
+
const collectDependencies = (value, into) => {
|
|
327
|
+
if (value.kind === 'derived')
|
|
328
|
+
into.push(value.alias);
|
|
329
|
+
for (const key of ['left', 'right', 'start', 'end', 'otherwise'])
|
|
330
|
+
if (value[key] && typeof value[key] === 'object')
|
|
331
|
+
collectDependencies(value[key], into);
|
|
332
|
+
for (const item of (value.values ?? []))
|
|
333
|
+
collectDependencies(item, into);
|
|
334
|
+
for (const branch of (value.branches ?? []))
|
|
335
|
+
collectDependencies(branch.then, into);
|
|
336
|
+
};
|
|
337
|
+
derived.forEach((entry) => collectDependencies(entry.expression, dependencies.get(entry.alias)));
|
|
338
|
+
const visiting = new Set();
|
|
339
|
+
const visited = new Set();
|
|
340
|
+
const visit = (alias) => { if (visiting.has(alias))
|
|
341
|
+
fail('invalid_identifier', 'derived', 'Derived expressions cannot form a cycle.'); if (visited.has(alias))
|
|
342
|
+
return; visiting.add(alias); dependencies.get(alias).forEach(visit); visiting.delete(alias); visited.add(alias); };
|
|
343
|
+
dependencies.forEach((_, alias) => visit(alias));
|
|
344
|
+
const having = predicate(root.having, 'having', 0, true);
|
|
345
|
+
const validateHaving = (item, path) => {
|
|
346
|
+
if (item.kind === 'all' || item.kind === 'any') {
|
|
347
|
+
item.children.forEach((child, index) => validateHaving(child, `${path}.children.${index}`));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (item.kind === 'not') {
|
|
351
|
+
validateHaving(item.child, `${path}.child`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const aggregateTarget = item.target;
|
|
355
|
+
if ((aggregateTarget.kind !== 'measure' || !measureAliases.has(aggregateTarget.alias)) && (aggregateTarget.kind !== 'derived' || stages.get(aggregateTarget.alias) !== 'aggregate'))
|
|
356
|
+
fail('invalid_identifier', `${path}.target.alias`, 'Aggregate target is unavailable.');
|
|
357
|
+
};
|
|
358
|
+
validateHaving(having, 'having');
|
|
359
|
+
const comparison = root.comparison === null ? null : (() => {
|
|
360
|
+
node('comparison');
|
|
361
|
+
const item = object(root.comparison, 'comparison', ['kind', 'dimension', 'targets']);
|
|
362
|
+
const dimension = object(item.dimension, 'comparison.dimension', ['source', 'field', 'bucket']);
|
|
363
|
+
const targets = boundedList(item.targets, 'comparison.targets').map((value, index) => {
|
|
364
|
+
const path = `comparison.targets.${index}`;
|
|
365
|
+
const target = object(value, path, ['kind', 'alias']);
|
|
366
|
+
const kind = oneOf(target.kind, `${path}.kind`, ['measure', 'derived']);
|
|
367
|
+
const alias = identifier(target.alias, `${path}.alias`, ALIAS_PATTERN);
|
|
368
|
+
if ((kind === 'measure' && !measureAliases.has(alias)) || (kind === 'derived' && stages.get(alias) !== 'aggregate'))
|
|
369
|
+
fail('invalid_identifier', `${path}.alias`, 'Comparison target is unavailable.');
|
|
370
|
+
return { kind, alias };
|
|
371
|
+
});
|
|
372
|
+
if (!targets.length)
|
|
373
|
+
fail('invalid_shape', 'comparison.targets', 'Comparison requires targets.');
|
|
374
|
+
return { kind: oneOf(item.kind, 'comparison.kind', ['previous_period', 'previous_year']), dimension: { source: source(dimension.source, aliases, 'comparison.dimension.source'), field: identifier(dimension.field, 'comparison.dimension.field', FIELD_PATTERN), bucket: oneOf(dimension.bucket, 'comparison.dimension.bucket', ['day', 'week', 'month', 'quarter', 'year']) }, targets };
|
|
375
|
+
})();
|
|
376
|
+
const sort = boundedList(root.sort, 'sort').map((value, index) => { const path = `sort.${index}`; const item = object(value, path, ['target', 'direction', 'nulls']); const target = plainObject(item.target, `${path}.target`); const kind = oneOf(target.kind, `${path}.target.kind`, ['field', 'dimension', 'measure', 'derived']); if (kind === 'field' || kind === 'dimension') {
|
|
377
|
+
object(target, `${path}.target`, ['kind', 'source', 'field']);
|
|
378
|
+
return { target: { kind, source: source(target.source, aliases, `${path}.target.source`), field: identifier(target.field, `${path}.target.field`, FIELD_PATTERN) }, direction: oneOf(item.direction, `${path}.direction`, ['asc', 'desc']), nulls: oneOf(item.nulls, `${path}.nulls`, ['first', 'last']) };
|
|
379
|
+
} object(target, `${path}.target`, ['kind', 'alias']); const alias = identifier(target.alias, `${path}.target.alias`, ALIAS_PATTERN); if ((kind === 'measure' && !measureAliases.has(alias)) || (kind === 'derived' && !stages.has(alias)))
|
|
380
|
+
fail('invalid_identifier', `${path}.target.alias`, 'Sort target is unavailable.'); return { target: { kind, alias }, direction: oneOf(item.direction, `${path}.direction`, ['asc', 'desc']), nulls: oneOf(item.nulls, `${path}.nulls`, ['first', 'last']) }; });
|
|
381
|
+
const limit = root.limit === null ? null : object(root.limit, 'limit', ['count']);
|
|
382
|
+
if (limit !== null && (!Number.isInteger(limit.count) || limit.count < 1 || limit.count > 1000))
|
|
383
|
+
fail('invalid_shape', 'limit.count', 'Limit is invalid.');
|
|
384
|
+
const result = object(root.result, 'result', ['shape']);
|
|
385
|
+
const shape = oneOf(result.shape, 'result.shape', ['rows', 'aggregate']);
|
|
386
|
+
if (shape === 'rows' && (fields.length === 0 || dimensions.length || measures.length || derived.some((entry) => entry.stage !== 'row')))
|
|
387
|
+
fail('result_shape_mismatch', 'result.shape', 'Row results cannot declare aggregate constructs.');
|
|
388
|
+
if (shape === 'aggregate' && (fields.length || !measures.length))
|
|
389
|
+
fail('result_shape_mismatch', 'result.shape', 'Aggregate results require measures and cannot declare row fields.');
|
|
390
|
+
return { schema_version: 4, sources, relationships: parsedRelationships, fields, timezone, where, dimensions, measures, having, derived, comparison, sort, limit: limit, result: { shape } };
|
|
391
|
+
}
|
|
392
|
+
function parseRelationship(value) {
|
|
393
|
+
if (value === null)
|
|
394
|
+
return null;
|
|
395
|
+
const relationship = object(value, 'relationship', ['key']);
|
|
396
|
+
return {
|
|
397
|
+
key: identifier(relationship.key, 'relationship.key', RELATIONSHIP_KEY_PATTERN),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function parseRelationships(value) {
|
|
401
|
+
const values = list(value, 'relationships');
|
|
402
|
+
if (values.length !== 2) {
|
|
403
|
+
fail('relationship_required', 'relationships', 'Resource Composition schema v2 requires exactly two relationships.');
|
|
404
|
+
}
|
|
405
|
+
const seen = new Set();
|
|
406
|
+
const relationships = values.map((value, index) => {
|
|
407
|
+
const path = `relationships.${index}`;
|
|
408
|
+
const relationship = object(value, path, ['key']);
|
|
409
|
+
const key = identifier(relationship.key, `${path}.key`, RELATIONSHIP_KEY_PATTERN);
|
|
410
|
+
if (seen.has(key))
|
|
411
|
+
fail('duplicate_identifier', `${path}.key`, 'Resource Composition entries must be unique.');
|
|
412
|
+
seen.add(key);
|
|
413
|
+
return { key };
|
|
414
|
+
});
|
|
415
|
+
return relationships;
|
|
416
|
+
}
|
|
417
|
+
function parseGraphRelationships(value, aliases) {
|
|
418
|
+
const values = list(value, 'relationships');
|
|
419
|
+
if (values.length === 0) {
|
|
420
|
+
fail('relationship_required', 'relationships', 'Resource Composition schema v3 requires at least one relationship.');
|
|
421
|
+
}
|
|
422
|
+
const seen = new Set();
|
|
423
|
+
return values.map((value, index) => {
|
|
424
|
+
const path = `relationships.${index}`;
|
|
425
|
+
const relationship = object(value, path, ['key', 'source', 'target']);
|
|
426
|
+
const key = identifier(relationship.key, `${path}.key`, RELATIONSHIP_KEY_PATTERN);
|
|
427
|
+
const sourceAlias = source(relationship.source, aliases, `${path}.source`);
|
|
428
|
+
const targetAlias = source(relationship.target, aliases, `${path}.target`);
|
|
429
|
+
const identity = `${key}\0${sourceAlias}\0${targetAlias}`;
|
|
430
|
+
if (seen.has(identity))
|
|
431
|
+
fail('duplicate_identifier', path, 'Resource Composition entries must be unique.');
|
|
432
|
+
seen.add(identity);
|
|
433
|
+
return { key, source: sourceAlias, target: targetAlias };
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function parseSelections(value, path, aliases) {
|
|
437
|
+
const selections = [];
|
|
438
|
+
const keys = new Set();
|
|
439
|
+
for (const [index, itemValue] of list(value, path).entries()) {
|
|
440
|
+
const itemPath = `${path}.${index}`;
|
|
441
|
+
const item = object(itemValue, itemPath, ['source', 'field']);
|
|
442
|
+
const selectedSource = source(item.source, aliases, `${itemPath}.source`);
|
|
443
|
+
const field = identifier(item.field, `${itemPath}.field`, FIELD_PATTERN);
|
|
444
|
+
const key = `${selectedSource}.${field}`;
|
|
445
|
+
if (keys.has(key))
|
|
446
|
+
fail('duplicate_identifier', itemPath, 'Resource Composition entries must be unique.');
|
|
447
|
+
keys.add(key);
|
|
448
|
+
selections.push({ source: selectedSource, field });
|
|
449
|
+
}
|
|
450
|
+
return selections;
|
|
451
|
+
}
|
|
452
|
+
function parseMeasure(value, index, aliases, seenAliases) {
|
|
453
|
+
const path = `measures.${index}`;
|
|
454
|
+
const measure = object(value, path, [
|
|
455
|
+
'alias',
|
|
456
|
+
'source',
|
|
457
|
+
'field',
|
|
458
|
+
'operation',
|
|
459
|
+
'result_type',
|
|
460
|
+
'null_behavior',
|
|
461
|
+
'unit',
|
|
462
|
+
]);
|
|
463
|
+
const alias = identifier(measure.alias, `${path}.alias`, ALIAS_PATTERN);
|
|
464
|
+
if (seenAliases.has(alias))
|
|
465
|
+
fail('duplicate_identifier', `${path}.alias`, 'Resource Composition entries must be unique.');
|
|
466
|
+
seenAliases.add(alias);
|
|
467
|
+
const selectedSource = source(measure.source, aliases, `${path}.source`);
|
|
468
|
+
const operation = oneOf(measure.operation, `${path}.operation`, ['count', 'distinct_count', 'sum', 'avg'], 'invalid_measure');
|
|
469
|
+
const resultType = oneOf(measure.result_type, `${path}.result_type`, ['integer', 'decimal'], 'invalid_measure');
|
|
470
|
+
const nullBehavior = oneOf(measure.null_behavior, `${path}.null_behavior`, ['zero', 'null_if_empty'], 'invalid_measure');
|
|
471
|
+
const unit = identifier(measure.unit, `${path}.unit`, UNIT_PATTERN, 'invalid_measure');
|
|
472
|
+
let field;
|
|
473
|
+
if (operation === 'count') {
|
|
474
|
+
if (measure.field !== null || resultType !== 'integer' || nullBehavior !== 'zero' || unit !== 'count') {
|
|
475
|
+
fail('invalid_measure', path, 'Count measures require a null field, integer result, zero null behavior, and count unit.');
|
|
476
|
+
}
|
|
477
|
+
field = null;
|
|
478
|
+
}
|
|
479
|
+
else if (operation === 'distinct_count') {
|
|
480
|
+
field = identifier(measure.field, `${path}.field`, FIELD_PATTERN);
|
|
481
|
+
if (resultType !== 'integer' || nullBehavior !== 'zero' || unit !== 'count') {
|
|
482
|
+
fail('invalid_measure', path, 'Distinct count measures require a field, integer result, zero null behavior, and count unit.');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
field = identifier(measure.field, `${path}.field`, FIELD_PATTERN);
|
|
487
|
+
if (resultType !== 'decimal' || nullBehavior !== 'null_if_empty' || unit === 'count') {
|
|
488
|
+
fail('invalid_measure', path, 'Sum and avg measures require a field, decimal result, null_if_empty behavior, and a declared unit.');
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
alias,
|
|
493
|
+
source: selectedSource,
|
|
494
|
+
field,
|
|
495
|
+
operation,
|
|
496
|
+
result_type: resultType,
|
|
497
|
+
null_behavior: nullBehavior,
|
|
498
|
+
unit,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function assertResultShape(shape, fields, dimensions, measures) {
|
|
502
|
+
if (shape === 'rows') {
|
|
503
|
+
if (fields.length === 0 || dimensions.length > 0 || measures.length > 0) {
|
|
504
|
+
fail('result_shape_mismatch', 'result.shape', 'Row results require fields and cannot declare dimensions or measures.');
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (fields.length > 0 || measures.length === 0) {
|
|
509
|
+
fail('result_shape_mismatch', 'result.shape', 'Aggregate results require measures and cannot declare row fields.');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function object(value, path, keys) {
|
|
513
|
+
const record = plainObject(value, path);
|
|
514
|
+
for (const key of Object.keys(record)) {
|
|
515
|
+
if (!keys.includes(key)) {
|
|
516
|
+
fail('unknown_key', path === '$' ? key : `${path}.${key}`, 'Resource Composition contains an unknown key.');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const key of keys) {
|
|
520
|
+
if (!Object.hasOwn(record, key)) {
|
|
521
|
+
fail('invalid_shape', path === '$' ? key : `${path}.${key}`, 'Resource Composition is missing a required key.');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return record;
|
|
525
|
+
}
|
|
526
|
+
function plainObject(value, path) {
|
|
527
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
528
|
+
fail('invalid_shape', path, 'Resource Composition value must be an object.');
|
|
529
|
+
}
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
function list(value, path) {
|
|
533
|
+
if (!Array.isArray(value))
|
|
534
|
+
fail('invalid_shape', path, 'Resource Composition value must be a list.');
|
|
535
|
+
return value;
|
|
536
|
+
}
|
|
537
|
+
function source(value, aliases, path) {
|
|
538
|
+
const selected = identifier(value, path, ALIAS_PATTERN);
|
|
539
|
+
if (!aliases.includes(selected)) {
|
|
540
|
+
fail('unknown_source_alias', path, 'Resource Composition source alias is not declared.');
|
|
541
|
+
}
|
|
542
|
+
return selected;
|
|
543
|
+
}
|
|
544
|
+
function identifier(value, path, pattern, code = 'invalid_identifier') {
|
|
545
|
+
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
546
|
+
fail(code, path, 'Resource Composition identifier is invalid.');
|
|
547
|
+
}
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
function oneOf(value, path, choices, code = 'invalid_shape') {
|
|
551
|
+
if (typeof value !== 'string' || !choices.includes(value)) {
|
|
552
|
+
fail(code, path, 'Resource Composition value is invalid.');
|
|
553
|
+
}
|
|
554
|
+
return value;
|
|
555
|
+
}
|
|
556
|
+
function scalar(value, path) {
|
|
557
|
+
if ((typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
|
|
558
|
+
|| (typeof value === 'number' && !Number.isFinite(value))) {
|
|
559
|
+
fail('invalid_filter_value', path, 'The eq filter value must be a scalar.');
|
|
560
|
+
}
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
563
|
+
function scalarList(value, path) {
|
|
564
|
+
const values = list(value, path);
|
|
565
|
+
if (values.length === 0)
|
|
566
|
+
fail('invalid_filter_value', path, 'The in filter requires a non-empty scalar list.');
|
|
567
|
+
return values.map((value) => scalar(value, path));
|
|
568
|
+
}
|
|
569
|
+
function fail(code, path, message) {
|
|
570
|
+
throw new CompositionSpecValidationError(code, path, message);
|
|
571
|
+
}
|
|
572
|
+
const ALIAS_PATTERN = /^[a-z][a-z0-9_]{0,31}$/;
|
|
573
|
+
const FIELD_PATTERN = /^[a-z][a-z0-9_]{0,159}$/;
|
|
574
|
+
const RELATIONSHIP_KEY_PATTERN = /^[a-z][a-z0-9_.-]{0,159}$/;
|
|
575
|
+
const RESOURCE_KEY_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*\.[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)*$/;
|
|
576
|
+
const UNIT_PATTERN = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ResourceCreateDestinationRegistration {
|
|
2
|
+
/** Stable Resource key, independent of the owning App's route layout. */
|
|
3
|
+
resourceKey: string;
|
|
4
|
+
/** Canonical host route for creating the Resource. */
|
|
5
|
+
route: string;
|
|
6
|
+
/** Permissions the owner requires before offering a create action. */
|
|
7
|
+
requiredPermissions: readonly string[];
|
|
8
|
+
ownerAppKey?: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ResourceCreateDestination extends ResourceCreateDestinationRegistration {
|
|
11
|
+
route: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ResourceCreateDestinationRegistry {
|
|
14
|
+
register(registration: ResourceCreateDestinationRegistration): void;
|
|
15
|
+
resolve(resourceKey: string, query?: Readonly<Record<string, string>>): ResourceCreateDestination | null;
|
|
16
|
+
resetForTests(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|