@nexia/sdk 0.5.0 → 0.6.1
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 +9 -0
- package/dist/index.d.ts +1 -1
- package/dist/resource-composition.d.ts +20 -61
- package/dist/resource-composition.js +51 -233
- package/package.json +22 -9
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @nexia/sdk
|
|
2
|
+
|
|
3
|
+
Public frontend contracts for Nexia App Packages. Import supported contracts from the package root, `/host` or `/testing`. This package does not contain the Nexia Core workspace runtime.
|
|
4
|
+
|
|
5
|
+
## Developer support / 개발자 지원
|
|
6
|
+
|
|
7
|
+
Report SDK, setup, CLI, Docker, AI-tool and sandbox problems in the [central developer support tracker](https://github.com/nexia-cloud-os/developer-support/issues). Search existing issues first and include package versions and minimal reproduction steps. Never attach credentials, private Core source or customer data.
|
|
8
|
+
|
|
9
|
+
SDK·개발환경·CLI·Docker·AI 도구·샌드박스 문제는 [통합 이슈 창구](https://github.com/nexia-cloud-os/developer-support/issues)에 신고하세요. 기존 이슈를 먼저 검색하고 패키지 버전과 최소 재현 단계를 포함하세요. 인증 정보, 비공개 Core 소스와 고객 데이터는 첨부하지 마세요.
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type { SubjectPopulation } from "./permissions";
|
|
|
19
19
|
export { formatNumber } from "./number";
|
|
20
20
|
export type { NumberLike } from "./number";
|
|
21
21
|
export { CompositionSpecValidationError, parseCompositionSpec, } from "./resource-composition";
|
|
22
|
-
export type { CompositionSpec
|
|
22
|
+
export type { CompositionSpec } from "./resource-composition";
|
|
23
23
|
export { RESOURCE_PICKER_STALE_TIME_MS, RESOURCE_REFERENCE_STATUSES, allowsManualReferenceFallback, isResourceReferenceBlocked, isResourceReferenceStatus, parseResourceReferenceOptionPage, resourcePickerRevalidation, resourceReferencePickerQueryKey, resourceReferencePickerQueryRoot, resourceReferenceOptionsRoute, savedResourceReferencePresentation, } from "./resource-reference";
|
|
24
24
|
export type { ResourceCreateDestination, ResourceCreateDestinationRegistration, ResourceCreateDestinationRegistry, } from "./resource-create-destination";
|
|
25
25
|
export type { ResourcePickerQueryState, ResourcePickerRefetchOptions, ResourcePickerRevalidation, ResourceReferenceAvailability, ResourceReferenceIdentity, ResourceReferenceOption, ResourceReferenceOptionPage, ResourceReferenceOptionPageMeta, ResourceReferenceOptionsResponse, ResourceReferencePickerQueryScope, ResourceReferenceSelectorContext, ResourceReferenceStatus, } from "./resource-reference";
|
|
@@ -1,71 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/** Strict, presentation-free intent for one bounded read-only Resource composition. */
|
|
2
|
+
interface CompositionSpecBase {
|
|
3
|
+
readonly schema_version: 1;
|
|
2
4
|
readonly sources: readonly {
|
|
3
5
|
readonly alias: string;
|
|
4
6
|
readonly resource_key: string;
|
|
5
7
|
}[];
|
|
6
|
-
readonly fields: readonly {
|
|
7
|
-
readonly source: string;
|
|
8
|
-
readonly field: string;
|
|
9
|
-
}[];
|
|
10
|
-
readonly filters: readonly {
|
|
11
|
-
readonly source: string;
|
|
12
|
-
readonly field: string;
|
|
13
|
-
readonly operator: 'eq' | 'in';
|
|
14
|
-
readonly value: string | number | boolean | readonly (string | number | boolean)[];
|
|
15
|
-
}[];
|
|
16
|
-
readonly dimensions: readonly {
|
|
17
|
-
readonly source: string;
|
|
18
|
-
readonly field: string;
|
|
19
|
-
readonly bucket: 'day' | 'week' | 'month' | null;
|
|
20
|
-
}[];
|
|
21
|
-
readonly measures: readonly {
|
|
22
|
-
readonly alias: string;
|
|
23
|
-
readonly source: string;
|
|
24
|
-
readonly field: string | null;
|
|
25
|
-
readonly operation: 'count' | 'distinct_count' | 'sum' | 'avg';
|
|
26
|
-
readonly result_type: 'integer' | 'decimal';
|
|
27
|
-
readonly null_behavior: 'zero' | 'null_if_empty';
|
|
28
|
-
readonly unit: string;
|
|
29
|
-
}[];
|
|
30
|
-
readonly result: {
|
|
31
|
-
readonly shape: 'rows' | 'aggregate';
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/** Strict, presentation-free intent for one bounded read-only Resource composition. */
|
|
35
|
-
export interface CompositionSpecV1 extends CompositionSpecBody {
|
|
36
|
-
readonly schema_version: 1;
|
|
37
|
-
readonly relationship: {
|
|
38
|
-
readonly key: string;
|
|
39
|
-
} | null;
|
|
40
|
-
readonly relationships?: never;
|
|
41
|
-
}
|
|
42
|
-
export interface CompositionSpecV2 extends CompositionSpecBody {
|
|
43
|
-
readonly schema_version: 2;
|
|
44
|
-
readonly relationship?: never;
|
|
45
|
-
readonly relationships: readonly [{
|
|
46
|
-
readonly key: string;
|
|
47
|
-
}, {
|
|
48
|
-
readonly key: string;
|
|
49
|
-
}];
|
|
50
|
-
}
|
|
51
|
-
export interface CompositionSpecV3 extends CompositionSpecBody {
|
|
52
|
-
readonly schema_version: 3;
|
|
53
|
-
readonly relationship?: never;
|
|
54
8
|
readonly relationships: readonly {
|
|
55
9
|
readonly key: string;
|
|
56
10
|
readonly source: string;
|
|
57
11
|
readonly target: string;
|
|
12
|
+
readonly match: 'preserve' | 'require' | 'exclude';
|
|
13
|
+
readonly time_field?: string;
|
|
58
14
|
}[];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
export interface CompositionSpecV4 {
|
|
62
|
-
readonly schema_version: 4;
|
|
63
|
-
readonly sources: CompositionSpecBody['sources'];
|
|
64
|
-
readonly relationships: readonly {
|
|
65
|
-
readonly key: string;
|
|
15
|
+
readonly population: {
|
|
16
|
+
readonly mode: 'anchor';
|
|
66
17
|
readonly source: string;
|
|
67
|
-
|
|
68
|
-
|
|
18
|
+
} | {
|
|
19
|
+
readonly mode: 'union' | 'intersection';
|
|
20
|
+
};
|
|
69
21
|
readonly fields: readonly ({
|
|
70
22
|
readonly source: string;
|
|
71
23
|
readonly field: string;
|
|
@@ -86,11 +38,18 @@ export interface CompositionSpecV4 {
|
|
|
86
38
|
readonly limit: {
|
|
87
39
|
readonly count: number;
|
|
88
40
|
} | null;
|
|
41
|
+
}
|
|
42
|
+
export type CompositionSpec = (CompositionSpecBase & {
|
|
89
43
|
readonly result: {
|
|
90
|
-
readonly shape: 'rows'
|
|
44
|
+
readonly shape: 'rows';
|
|
91
45
|
};
|
|
92
|
-
|
|
93
|
-
|
|
46
|
+
readonly row_source: string;
|
|
47
|
+
}) | (CompositionSpecBase & {
|
|
48
|
+
readonly result: {
|
|
49
|
+
readonly shape: 'aggregate';
|
|
50
|
+
};
|
|
51
|
+
readonly row_source?: never;
|
|
52
|
+
});
|
|
94
53
|
export declare class CompositionSpecValidationError extends Error {
|
|
95
54
|
readonly code: 'invalid_shape' | 'unsupported_schema_version' | 'unknown_key' | 'invalid_identifier' | 'duplicate_identifier' | 'invalid_source_count' | 'unknown_source_alias' | 'relationship_required' | 'relationship_not_allowed' | 'invalid_filter_value' | 'invalid_measure' | 'result_shape_mismatch';
|
|
96
55
|
readonly path: string;
|
|
@@ -14,122 +14,13 @@ export class CompositionSpecValidationError extends Error {
|
|
|
14
14
|
*/
|
|
15
15
|
export function parseCompositionSpec(value) {
|
|
16
16
|
const candidate = plainObject(value, '$');
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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']);
|
|
17
|
+
if (candidate.schema_version !== 1)
|
|
18
|
+
fail('unsupported_schema_version', 'schema_version', 'Resource Composition schema_version must be 1.');
|
|
19
|
+
const resultInput = object(candidate.result, 'result', ['shape']);
|
|
20
|
+
const declaredShape = oneOf(resultInput.shape, 'result.shape', ['rows', 'aggregate']);
|
|
21
|
+
const root = object(candidate, '$', declaredShape === 'rows'
|
|
22
|
+
? ['schema_version', 'sources', 'relationships', 'population', 'row_source', 'fields', 'timezone', 'where', 'dimensions', 'measures', 'having', 'derived', 'comparison', 'sort', 'limit', 'result']
|
|
23
|
+
: ['schema_version', 'sources', 'relationships', 'population', 'fields', 'timezone', 'where', 'dimensions', 'measures', 'having', 'derived', 'comparison', 'sort', 'limit', 'result']);
|
|
133
24
|
let nodes = 0;
|
|
134
25
|
const boundedList = (value, path) => {
|
|
135
26
|
const values = list(value, path);
|
|
@@ -141,7 +32,7 @@ function parseV4(candidate) {
|
|
|
141
32
|
if (depth > 8 || ++nodes > 100)
|
|
142
33
|
fail('invalid_shape', path, 'Resource Composition is too complex.');
|
|
143
34
|
};
|
|
144
|
-
const
|
|
35
|
+
const scalar = (value, path) => {
|
|
145
36
|
if ((typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') || (typeof value === 'number' && !Number.isFinite(value)) || (typeof value === 'string' && value.length > 512))
|
|
146
37
|
fail('invalid_filter_value', path, 'Resource Composition scalar is invalid.');
|
|
147
38
|
return value;
|
|
@@ -185,37 +76,41 @@ function parseV4(candidate) {
|
|
|
185
76
|
const values = boundedList(item.value, `${path}.value`);
|
|
186
77
|
if (values.length === 0)
|
|
187
78
|
fail('invalid_filter_value', `${path}.value`, 'The in filter requires a non-empty scalar list.');
|
|
188
|
-
predicateValue = values.map((entry) =>
|
|
79
|
+
predicateValue = values.map((entry) => scalar(entry, `${path}.value`));
|
|
189
80
|
}
|
|
190
81
|
else if (operator === 'between') {
|
|
191
82
|
const bounds = object(item.value, `${path}.value`, ['from', 'to']);
|
|
192
|
-
|
|
193
|
-
|
|
83
|
+
scalar(bounds.from, `${path}.value.from`);
|
|
84
|
+
scalar(bounds.to, `${path}.value.to`);
|
|
194
85
|
}
|
|
195
86
|
else if (operator === 'is_null' || operator === 'not_null') {
|
|
196
87
|
if (item.value !== null)
|
|
197
88
|
fail('invalid_filter_value', `${path}.value`, 'Null predicate requires null.');
|
|
198
89
|
}
|
|
199
90
|
else
|
|
200
|
-
|
|
91
|
+
scalar(item.value, `${path}.value`);
|
|
201
92
|
if (!aggregate)
|
|
202
93
|
return { kind, target: target(item.target, `${path}.target`), operator, value: predicateValue };
|
|
203
94
|
const aggregateTarget = object(item.target, `${path}.target`, ['kind', 'alias']);
|
|
204
95
|
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
96
|
};
|
|
97
|
+
const sourceAliases = new Set();
|
|
206
98
|
const sources = boundedList(root.sources, 'sources').map((value, index) => {
|
|
207
|
-
const
|
|
208
|
-
|
|
99
|
+
const path = `sources.${index}`;
|
|
100
|
+
const item = object(value, path, ['alias', 'resource_key']);
|
|
101
|
+
const alias = identifier(item.alias, `${path}.alias`, ALIAS_PATTERN);
|
|
102
|
+
if (sourceAliases.has(alias))
|
|
103
|
+
fail('duplicate_identifier', path, 'Resource Composition source aliases must be unique.');
|
|
104
|
+
sourceAliases.add(alias);
|
|
105
|
+
return { alias, resource_key: identifier(item.resource_key, `${path}.resource_key`, RESOURCE_KEY_PATTERN) };
|
|
209
106
|
});
|
|
210
107
|
if (sources.length === 0)
|
|
211
|
-
fail('invalid_source_count', 'sources', 'Resource Composition
|
|
108
|
+
fail('invalid_source_count', 'sources', 'Resource Composition requires at least one source.');
|
|
212
109
|
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
110
|
const relationships = boundedList(root.relationships, 'relationships');
|
|
216
111
|
if (sources.length === 1 && relationships.length !== 0)
|
|
217
112
|
fail('relationship_not_allowed', 'relationships', 'Single-source Resource Composition cannot declare relationships.');
|
|
218
|
-
const parsedRelationships =
|
|
113
|
+
const parsedRelationships = parseGraphRelationships(relationships, aliases);
|
|
219
114
|
const timezone = identifier(root.timezone, 'timezone', /^[A-Za-z_]+(?:\/[A-Za-z_+-]+)+$/);
|
|
220
115
|
const fields = boundedList(root.fields, 'fields').map((value, index) => {
|
|
221
116
|
node(`fields.${index}`);
|
|
@@ -227,6 +122,13 @@ function parseV4(candidate) {
|
|
|
227
122
|
object(item, `fields.${index}`, ['source', 'field']);
|
|
228
123
|
return { source: source(item.source, aliases, `fields.${index}.source`), field: identifier(item.field, `fields.${index}.field`, FIELD_PATTERN) };
|
|
229
124
|
});
|
|
125
|
+
const fieldKeys = new Set();
|
|
126
|
+
fields.forEach((field, index) => {
|
|
127
|
+
const key = 'derived' in field ? `derived:${field.derived}` : `${field.source}.${field.field}`;
|
|
128
|
+
if (fieldKeys.has(key))
|
|
129
|
+
fail('duplicate_identifier', `fields.${index}`, 'Resource Composition entries must be unique.');
|
|
130
|
+
fieldKeys.add(key);
|
|
131
|
+
});
|
|
230
132
|
const where = predicate(root.where, 'where');
|
|
231
133
|
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
134
|
const measures = boundedList(root.measures, 'measures').map((value, index) => {
|
|
@@ -387,128 +289,44 @@ function parseV4(candidate) {
|
|
|
387
289
|
fail('result_shape_mismatch', 'result.shape', 'Row results cannot declare aggregate constructs.');
|
|
388
290
|
if (shape === 'aggregate' && (fields.length || !measures.length))
|
|
389
291
|
fail('result_shape_mismatch', 'result.shape', 'Aggregate results require measures and cannot declare row fields.');
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
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;
|
|
292
|
+
const populationValue = plainObject(root.population, 'population');
|
|
293
|
+
const population = populationValue.mode === 'anchor'
|
|
294
|
+
? (() => { object(populationValue, 'population', ['mode', 'source']); return { mode: 'anchor', source: source(populationValue.source, aliases, 'population.source') }; })()
|
|
295
|
+
: (() => { object(populationValue, 'population', ['mode']); const mode = oneOf(populationValue.mode, 'population.mode', ['union', 'intersection']); if (shape !== 'aggregate')
|
|
296
|
+
fail('result_shape_mismatch', 'population.mode', 'Union and intersection populations require aggregate results.'); return { mode }; })();
|
|
297
|
+
const row_source = shape === 'rows' ? source(root.row_source, aliases, 'row_source') : undefined;
|
|
298
|
+
return { schema_version: 1, sources, relationships: parsedRelationships, population, ...(row_source === undefined ? {} : { row_source }), fields, timezone, where, dimensions, measures, having, derived, comparison, sort, limit, result: { shape } };
|
|
416
299
|
}
|
|
417
300
|
function parseGraphRelationships(value, aliases) {
|
|
418
301
|
const values = list(value, 'relationships');
|
|
419
|
-
if (values.length === 0) {
|
|
420
|
-
fail('relationship_required', 'relationships', 'Resource Composition
|
|
302
|
+
if (values.length === 0 && aliases.length > 1) {
|
|
303
|
+
fail('relationship_required', 'relationships', 'Multi-source Resource Composition requires at least one relationship.');
|
|
421
304
|
}
|
|
422
305
|
const seen = new Set();
|
|
423
306
|
return values.map((value, index) => {
|
|
424
307
|
const path = `relationships.${index}`;
|
|
425
|
-
const relationship =
|
|
308
|
+
const relationship = plainObject(value, path);
|
|
309
|
+
const { time_field: timeField, ...requiredRelationship } = relationship;
|
|
310
|
+
object(requiredRelationship, path, ['key', 'source', 'target', 'match']);
|
|
426
311
|
const key = identifier(relationship.key, `${path}.key`, RELATIONSHIP_KEY_PATTERN);
|
|
427
312
|
const sourceAlias = source(relationship.source, aliases, `${path}.source`);
|
|
428
313
|
const targetAlias = source(relationship.target, aliases, `${path}.target`);
|
|
314
|
+
const match = oneOf(relationship.match, `${path}.match`, ['preserve', 'require', 'exclude']);
|
|
429
315
|
const identity = `${key}\0${sourceAlias}\0${targetAlias}`;
|
|
430
316
|
if (seen.has(identity))
|
|
431
317
|
fail('duplicate_identifier', path, 'Resource Composition entries must be unique.');
|
|
432
318
|
seen.add(identity);
|
|
433
|
-
return {
|
|
319
|
+
return {
|
|
320
|
+
key,
|
|
321
|
+
source: sourceAlias,
|
|
322
|
+
target: targetAlias,
|
|
323
|
+
match,
|
|
324
|
+
...(Object.hasOwn(relationship, 'time_field')
|
|
325
|
+
? { time_field: identifier(timeField, `${path}.time_field`, FIELD_PATTERN) }
|
|
326
|
+
: {}),
|
|
327
|
+
};
|
|
434
328
|
});
|
|
435
329
|
}
|
|
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
330
|
function object(value, path, keys) {
|
|
513
331
|
const record = plainObject(value, path);
|
|
514
332
|
for (const key of Object.keys(record)) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexia/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Frontend contracts for Nexia App Packages",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"packageManager": "pnpm@11.17.0",
|
|
7
8
|
"files": [
|
|
8
9
|
"dist"
|
|
9
10
|
],
|
|
@@ -27,6 +28,17 @@
|
|
|
27
28
|
}
|
|
28
29
|
},
|
|
29
30
|
"sideEffects": false,
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node scripts/build.mjs",
|
|
33
|
+
"prepack": "npm run build",
|
|
34
|
+
"test": "npm run build && node tests/WireValuesTest.mjs && node tests/ResourceCompositionFrontendContractsTest.mjs && node scripts/organization-target-contract.mjs",
|
|
35
|
+
"test:wire-values": "node tests/WireValuesTest.mjs",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
37
|
+
"test:resource-composition": "pnpm run build && node tests/ResourceCompositionFrontendContractsTest.mjs",
|
|
38
|
+
"test:organization-target": "pnpm run build && node scripts/organization-target-contract.mjs",
|
|
39
|
+
"build:watch": "node scripts/build.mjs --watch",
|
|
40
|
+
"test:build-watch": "node --test tests/BuildWatchTest.mjs"
|
|
41
|
+
},
|
|
30
42
|
"peerDependencies": {
|
|
31
43
|
"@tanstack/react-query": "^5.99.0",
|
|
32
44
|
"axios": "^1.11.0",
|
|
@@ -40,14 +52,15 @@
|
|
|
40
52
|
"typescript": "^5.8.3"
|
|
41
53
|
},
|
|
42
54
|
"publishConfig": {
|
|
43
|
-
"registry": "https://
|
|
55
|
+
"registry": "https://registry.npmjs.org/",
|
|
56
|
+
"access": "public"
|
|
44
57
|
},
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/nexia-cloud-os/app-sdk.git",
|
|
61
|
+
"directory": "packages/react"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/nexia-cloud-os/developer-support/issues"
|
|
52
65
|
}
|
|
53
66
|
}
|