@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 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, CompositionSpecV3, CompositionSpecV4 } from "./resource-composition";
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
- interface CompositionSpecBody {
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
- /** Advanced reports retain one strict CompositionSpec wire; Core gates live capabilities. */
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
- readonly target: string;
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' | 'aggregate';
44
+ readonly shape: 'rows';
91
45
  };
92
- }
93
- export type CompositionSpec = CompositionSpecV1 | CompositionSpecV2 | CompositionSpecV3 | CompositionSpecV4;
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
- 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']);
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 scalarV4 = (value, path) => {
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) => scalarV4(entry, `${path}.value`));
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
- scalarV4(bounds.from, `${path}.value.from`);
193
- scalarV4(bounds.to, `${path}.value.to`);
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
- scalarV4(item.value, `${path}.value`);
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 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) };
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 schema v4 requires at least one source.');
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 = sources.length === 1 ? [] : parseGraphRelationships(relationships, aliases);
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
- 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;
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 schema v3 requires at least one relationship.');
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 = object(value, path, ['key', 'source', 'target']);
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 { key, source: sourceAlias, target: targetAlias };
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.5.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://npm.pkg.github.com"
55
+ "registry": "https://registry.npmjs.org/",
56
+ "access": "public"
44
57
  },
45
- "scripts": {
46
- "build": "node scripts/build.mjs",
47
- "test": "npm run build && node tests/WireValuesTest.mjs && node tests/ResourceCompositionFrontendContractsTest.mjs && node scripts/organization-target-contract.mjs",
48
- "test:wire-values": "node tests/WireValuesTest.mjs",
49
- "typecheck": "tsc -p tsconfig.json --noEmit",
50
- "test:resource-composition": "pnpm run build && node tests/ResourceCompositionFrontendContractsTest.mjs",
51
- "test:organization-target": "pnpm run build && node scripts/organization-target-contract.mjs"
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
  }