@unito/integration-api 8.0.9 → 8.2.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.
@@ -1,4 +1,4 @@
1
- import { FieldSchema, FieldValues, Semantic } from './types.js';
1
+ import { FieldSchema, FieldValues, ResolveItem, Semantic } from './types.js';
2
2
  declare const ValidFieldValueEntryBrand: unique symbol;
3
3
  /**
4
4
  * A single field value that has been validated against its FieldSchema.
@@ -33,19 +33,21 @@ export declare function getUnsafeValue<T = unknown>(field: FieldSchema, values:
33
33
  export declare function getSafeValue<T = unknown>(field: FieldSchema, values: FieldValues): ReturnType<typeof getUnsafeValue<T>> | undefined;
34
34
  /**
35
35
  * Returns a field value by its name, traversing dotted paths through OBJECT and REFERENCE fields.
36
+ *
37
+ * Pass options.resolveItem to resolve any RelationPointer crossed along the path by fetching the pointed item.
36
38
  */
37
- export declare function findValueByName<T = unknown>(fields: FieldSchema[], values: FieldValues, fieldName: string): ReturnType<typeof getSafeValue<T>>;
38
- interface InheritedFieldProperties {
39
- isArray?: boolean | undefined;
40
- canSetOnCreate?: boolean | undefined;
41
- canSetOnUpdate?: boolean | undefined;
42
- nullable?: boolean | undefined;
43
- }
39
+ export declare function findValueByName<T = unknown>(fields: FieldSchema[], values: FieldValues, fieldName: string, options?: {
40
+ resolveItem?: ResolveItem;
41
+ }): Promise<ReturnType<typeof getSafeValue<T>>>;
44
42
  /**
45
43
  * Returns a field schema by its name, traversing dotted paths through OBJECT and REFERENCE fields.
46
44
  * Also accepts `semantic:` prefixed names and sanitized names (see sanitizeFieldName).
45
+ *
46
+ * Pass options.resolveItem to resolve any RelationPointer crossed along the path by fetching the pointed item.
47
47
  */
48
- export declare function findFieldByName(fields: FieldSchema[], fieldName: string, inheritedProperties?: InheritedFieldProperties): FieldSchema | undefined;
48
+ export declare function findFieldByName(fields: FieldSchema[], fieldName: string, options?: {
49
+ resolveItem?: ResolveItem;
50
+ }): Promise<FieldSchema | undefined>;
49
51
  /**
50
52
  * Returns a field schema by its semantic.
51
53
  *
@@ -1,4 +1,4 @@
1
- import { FieldValueTypes, Semantics } from './types.js';
1
+ import { FieldValueTypes, Semantics, } from './types.js';
2
2
  import { isItemSummary, isObject, isRelationPointer } from './guards.js';
3
3
  /**
4
4
  * Escapes characters that would otherwise break dotted field name path parsing.
@@ -168,20 +168,40 @@ export function getSafeValue(field, values) {
168
168
  return undefined;
169
169
  }
170
170
  }
171
+ /**
172
+ * Returns the field schemas behind a REFERENCE field, resolving a RelationPointer through options.resolveItem.
173
+ * Returns undefined when the pointer cannot be resolved.
174
+ *
175
+ * @throws Error if the reference is an unpopulated RelationPointer and no resolveItem is provided.
176
+ */
177
+ async function resolveReferenceFields(field, fields, options) {
178
+ if (isRelationPointer(field.reference)) {
179
+ if (!options.resolveItem) {
180
+ throw new Error(`Reference field "${field.name}" is a relation pointer that was not populated`);
181
+ }
182
+ const { itemPath, relationName } = field.reference;
183
+ const item = await options.resolveItem(itemPath);
184
+ const relation = item?.relations.find(relation => relation.name === relationName);
185
+ return relation?.schema.fields;
186
+ }
187
+ return field.reference.schema === '__self' ? fields : field.reference.schema.fields;
188
+ }
171
189
  /**
172
190
  * Returns a field value by its name, traversing dotted paths through OBJECT and REFERENCE fields.
191
+ *
192
+ * Pass options.resolveItem to resolve any RelationPointer crossed along the path by fetching the pointed item.
173
193
  */
174
- export function findValueByName(fields, values, fieldName) {
194
+ export async function findValueByName(fields, values, fieldName, options = {}) {
175
195
  const [baseFieldName, ...subFieldNames] = fieldName.split('.');
176
196
  if (baseFieldName === undefined) {
177
197
  return undefined;
178
198
  }
179
- const baseField = findFieldByName(fields, baseFieldName);
199
+ const baseField = await findFieldByName(fields, baseFieldName, options);
180
200
  if (!baseField) {
181
201
  if (subFieldNames.length === 0) {
182
202
  return undefined;
183
203
  }
184
- const literalField = findFieldByName(fields, fieldName);
204
+ const literalField = await findFieldByName(fields, fieldName, options);
185
205
  return literalField ? getSafeValue(literalField, values) : undefined;
186
206
  }
187
207
  const baseFieldValue = getSafeValue(baseField, values);
@@ -191,30 +211,30 @@ export function findValueByName(fields, values, fieldName) {
191
211
  if (baseField.type === FieldValueTypes.OBJECT) {
192
212
  const subFieldName = subFieldNames.join('.');
193
213
  if (isObject(baseFieldValue)) {
194
- return findValueByName(baseField.fields, baseFieldValue, subFieldName);
214
+ return findValueByName(baseField.fields, baseFieldValue, subFieldName, options);
195
215
  }
196
216
  if (Array.isArray(baseFieldValue)) {
197
- return baseFieldValue
198
- .map(value => (isObject(value) ? findValueByName(baseField.fields, value, subFieldName) : undefined))
199
- .filter(value => value !== undefined);
217
+ const resolved = await Promise.all(baseFieldValue.map(value => isObject(value) ? findValueByName(baseField.fields, value, subFieldName, options) : undefined));
218
+ return resolved.filter(value => value !== undefined);
200
219
  }
201
220
  }
202
221
  if (baseField.type === FieldValueTypes.REFERENCE) {
203
- if (isRelationPointer(baseField.reference)) {
204
- throw new Error(`Reference field "${baseField.name}" is a relation pointer that was not populated`);
222
+ const referenceFields = await resolveReferenceFields(baseField, fields, options);
223
+ if (!referenceFields) {
224
+ return undefined;
205
225
  }
206
226
  if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
207
227
  subFieldNames.shift();
208
228
  }
209
229
  const subFieldName = subFieldNames.join('.');
210
- const referenceFields = baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields;
211
230
  if (isItemSummary(baseFieldValue)) {
212
- return findValueByName(referenceFields, baseFieldValue.fields ?? {}, subFieldName);
231
+ return findValueByName(referenceFields, baseFieldValue.fields ?? {}, subFieldName, options);
213
232
  }
214
233
  if (Array.isArray(baseFieldValue)) {
215
- return baseFieldValue
216
- .map(value => isItemSummary(value) ? findValueByName(referenceFields, value.fields ?? {}, subFieldName) : undefined)
217
- .filter(value => value !== undefined);
234
+ const resolved = await Promise.all(baseFieldValue.map(value => isItemSummary(value)
235
+ ? findValueByName(referenceFields, value.fields ?? {}, subFieldName, options)
236
+ : undefined));
237
+ return resolved.filter(value => value !== undefined);
218
238
  }
219
239
  }
220
240
  return undefined;
@@ -246,8 +266,13 @@ function mergeInheritedProperties(field, inherited) {
246
266
  /**
247
267
  * Returns a field schema by its name, traversing dotted paths through OBJECT and REFERENCE fields.
248
268
  * Also accepts `semantic:` prefixed names and sanitized names (see sanitizeFieldName).
269
+ *
270
+ * Pass options.resolveItem to resolve any RelationPointer crossed along the path by fetching the pointed item.
249
271
  */
250
- export function findFieldByName(fields, fieldName, inheritedProperties = {}) {
272
+ export function findFieldByName(fields, fieldName, options = {}) {
273
+ return baseFindFieldByName(fields, fieldName, {}, options);
274
+ }
275
+ async function baseFindFieldByName(fields, fieldName, inheritedProperties, options) {
251
276
  const [baseFieldName, ...subFieldNames] = fieldName.split('.');
252
277
  if (baseFieldName === undefined) {
253
278
  return undefined;
@@ -266,26 +291,27 @@ export function findFieldByName(fields, fieldName, inheritedProperties = {}) {
266
291
  return baseField;
267
292
  }
268
293
  if (baseField.type === FieldValueTypes.OBJECT) {
269
- return findFieldByName(baseField.fields, subFieldNames.join('.'), {
294
+ return baseFindFieldByName(baseField.fields, subFieldNames.join('.'), {
270
295
  isArray: baseField.isArray,
271
296
  canSetOnCreate: baseField.canSetOnCreate,
272
297
  canSetOnUpdate: baseField.canSetOnUpdate,
273
298
  nullable: baseField.nullable,
274
- });
299
+ }, options);
275
300
  }
276
301
  if (baseField.type === FieldValueTypes.REFERENCE) {
277
- if (isRelationPointer(baseField.reference)) {
278
- throw new Error(`Reference field "${baseField.name}" is a relation pointer that was not populated`);
302
+ const referenceFields = await resolveReferenceFields(baseField, fields, options);
303
+ if (!referenceFields) {
304
+ return undefined;
279
305
  }
280
306
  if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
281
307
  subFieldNames.shift();
282
308
  }
283
- return findFieldByName(baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields, subFieldNames.join('.'), {
309
+ return baseFindFieldByName(referenceFields, subFieldNames.join('.'), {
284
310
  isArray: baseField.isArray,
285
311
  canSetOnCreate: baseField.canSetOnCreate,
286
312
  canSetOnUpdate: baseField.canSetOnUpdate,
287
313
  nullable: baseField.nullable,
288
- });
314
+ }, options);
289
315
  }
290
316
  return undefined;
291
317
  }
@@ -874,20 +874,40 @@ function getSafeValue(field, values) {
874
874
  return undefined;
875
875
  }
876
876
  }
877
+ /**
878
+ * Returns the field schemas behind a REFERENCE field, resolving a RelationPointer through options.resolveItem.
879
+ * Returns undefined when the pointer cannot be resolved.
880
+ *
881
+ * @throws Error if the reference is an unpopulated RelationPointer and no resolveItem is provided.
882
+ */
883
+ async function resolveReferenceFields(field, fields, options) {
884
+ if (isRelationPointer(field.reference)) {
885
+ if (!options.resolveItem) {
886
+ throw new Error(`Reference field "${field.name}" is a relation pointer that was not populated`);
887
+ }
888
+ const { itemPath, relationName } = field.reference;
889
+ const item = await options.resolveItem(itemPath);
890
+ const relation = item?.relations.find(relation => relation.name === relationName);
891
+ return relation?.schema.fields;
892
+ }
893
+ return field.reference.schema === '__self' ? fields : field.reference.schema.fields;
894
+ }
877
895
  /**
878
896
  * Returns a field value by its name, traversing dotted paths through OBJECT and REFERENCE fields.
897
+ *
898
+ * Pass options.resolveItem to resolve any RelationPointer crossed along the path by fetching the pointed item.
879
899
  */
880
- function findValueByName(fields, values, fieldName) {
900
+ async function findValueByName(fields, values, fieldName, options = {}) {
881
901
  const [baseFieldName, ...subFieldNames] = fieldName.split('.');
882
902
  if (baseFieldName === undefined) {
883
903
  return undefined;
884
904
  }
885
- const baseField = findFieldByName(fields, baseFieldName);
905
+ const baseField = await findFieldByName(fields, baseFieldName, options);
886
906
  if (!baseField) {
887
907
  if (subFieldNames.length === 0) {
888
908
  return undefined;
889
909
  }
890
- const literalField = findFieldByName(fields, fieldName);
910
+ const literalField = await findFieldByName(fields, fieldName, options);
891
911
  return literalField ? getSafeValue(literalField, values) : undefined;
892
912
  }
893
913
  const baseFieldValue = getSafeValue(baseField, values);
@@ -897,30 +917,30 @@ function findValueByName(fields, values, fieldName) {
897
917
  if (baseField.type === FieldValueTypes.OBJECT) {
898
918
  const subFieldName = subFieldNames.join('.');
899
919
  if (isObject(baseFieldValue)) {
900
- return findValueByName(baseField.fields, baseFieldValue, subFieldName);
920
+ return findValueByName(baseField.fields, baseFieldValue, subFieldName, options);
901
921
  }
902
922
  if (Array.isArray(baseFieldValue)) {
903
- return baseFieldValue
904
- .map(value => (isObject(value) ? findValueByName(baseField.fields, value, subFieldName) : undefined))
905
- .filter(value => value !== undefined);
923
+ const resolved = await Promise.all(baseFieldValue.map(value => isObject(value) ? findValueByName(baseField.fields, value, subFieldName, options) : undefined));
924
+ return resolved.filter(value => value !== undefined);
906
925
  }
907
926
  }
908
927
  if (baseField.type === FieldValueTypes.REFERENCE) {
909
- if (isRelationPointer(baseField.reference)) {
910
- throw new Error(`Reference field "${baseField.name}" is a relation pointer that was not populated`);
928
+ const referenceFields = await resolveReferenceFields(baseField, fields, options);
929
+ if (!referenceFields) {
930
+ return undefined;
911
931
  }
912
932
  if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
913
933
  subFieldNames.shift();
914
934
  }
915
935
  const subFieldName = subFieldNames.join('.');
916
- const referenceFields = baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields;
917
936
  if (isItemSummary(baseFieldValue)) {
918
- return findValueByName(referenceFields, baseFieldValue.fields ?? {}, subFieldName);
937
+ return findValueByName(referenceFields, baseFieldValue.fields ?? {}, subFieldName, options);
919
938
  }
920
939
  if (Array.isArray(baseFieldValue)) {
921
- return baseFieldValue
922
- .map(value => isItemSummary(value) ? findValueByName(referenceFields, value.fields ?? {}, subFieldName) : undefined)
923
- .filter(value => value !== undefined);
940
+ const resolved = await Promise.all(baseFieldValue.map(value => isItemSummary(value)
941
+ ? findValueByName(referenceFields, value.fields ?? {}, subFieldName, options)
942
+ : undefined));
943
+ return resolved.filter(value => value !== undefined);
924
944
  }
925
945
  }
926
946
  return undefined;
@@ -952,8 +972,13 @@ function mergeInheritedProperties(field, inherited) {
952
972
  /**
953
973
  * Returns a field schema by its name, traversing dotted paths through OBJECT and REFERENCE fields.
954
974
  * Also accepts `semantic:` prefixed names and sanitized names (see sanitizeFieldName).
975
+ *
976
+ * Pass options.resolveItem to resolve any RelationPointer crossed along the path by fetching the pointed item.
955
977
  */
956
- function findFieldByName(fields, fieldName, inheritedProperties = {}) {
978
+ function findFieldByName(fields, fieldName, options = {}) {
979
+ return baseFindFieldByName(fields, fieldName, {}, options);
980
+ }
981
+ async function baseFindFieldByName(fields, fieldName, inheritedProperties, options) {
957
982
  const [baseFieldName, ...subFieldNames] = fieldName.split('.');
958
983
  if (baseFieldName === undefined) {
959
984
  return undefined;
@@ -972,26 +997,27 @@ function findFieldByName(fields, fieldName, inheritedProperties = {}) {
972
997
  return baseField;
973
998
  }
974
999
  if (baseField.type === FieldValueTypes.OBJECT) {
975
- return findFieldByName(baseField.fields, subFieldNames.join('.'), {
1000
+ return baseFindFieldByName(baseField.fields, subFieldNames.join('.'), {
976
1001
  isArray: baseField.isArray,
977
1002
  canSetOnCreate: baseField.canSetOnCreate,
978
1003
  canSetOnUpdate: baseField.canSetOnUpdate,
979
1004
  nullable: baseField.nullable,
980
- });
1005
+ }, options);
981
1006
  }
982
1007
  if (baseField.type === FieldValueTypes.REFERENCE) {
983
- if (isRelationPointer(baseField.reference)) {
984
- throw new Error(`Reference field "${baseField.name}" is a relation pointer that was not populated`);
1008
+ const referenceFields = await resolveReferenceFields(baseField, fields, options);
1009
+ if (!referenceFields) {
1010
+ return undefined;
985
1011
  }
986
1012
  if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
987
1013
  subFieldNames.shift();
988
1014
  }
989
- return findFieldByName(baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields, subFieldNames.join('.'), {
1015
+ return baseFindFieldByName(referenceFields, subFieldNames.join('.'), {
990
1016
  isArray: baseField.isArray,
991
1017
  canSetOnCreate: baseField.canSetOnCreate,
992
1018
  canSetOnUpdate: baseField.canSetOnUpdate,
993
1019
  nullable: baseField.nullable,
994
- });
1020
+ }, options);
995
1021
  }
996
1022
  return undefined;
997
1023
  }
@@ -1016,7 +1042,7 @@ function findFieldBySemantic(fields, semantic) {
1016
1042
  /**
1017
1043
  * JSONPath parser that returns a relation that is guaranteed to have its schema populated.
1018
1044
  */
1019
- function findRelationByJSONPath(item, query) {
1045
+ async function findRelationByJSONPath(item, query, options = {}) {
1020
1046
  const tokens = parseJSONPath(query);
1021
1047
  const schemas = [];
1022
1048
  let current = item;
@@ -1033,6 +1059,16 @@ function findRelationByJSONPath(item, query) {
1033
1059
  schemas.push(result['schema']);
1034
1060
  }
1035
1061
  current = result;
1062
+ if (isRelationPointer(current) && options.resolveItem) {
1063
+ const { itemPath, relationName } = current;
1064
+ const item = await options.resolveItem(itemPath);
1065
+ if (item) {
1066
+ current = item.relations.find(relation => relation.name === relationName);
1067
+ }
1068
+ else {
1069
+ return undefined;
1070
+ }
1071
+ }
1036
1072
  }
1037
1073
  if (isRelation(current, { strict: false })) {
1038
1074
  return current;
@@ -2,4 +2,6 @@ import * as Api from './types.js';
2
2
  /**
3
3
  * JSONPath parser that returns a relation that is guaranteed to have its schema populated.
4
4
  */
5
- export declare function findRelationByJSONPath(item: Api.Item, query: string): Api.Relation | Api.RelationWithPopulatedSchema<Api.RelationSummary> | Api.RelationWithPopulatedSchema<Api.ReferenceRelation> | undefined;
5
+ export declare function findRelationByJSONPath(item: Api.Item, query: string, options?: {
6
+ resolveItem?: Api.ResolveItem;
7
+ }): Promise<Api.Relation | Api.RelationWithPopulatedSchema<Api.RelationSummary> | Api.RelationWithPopulatedSchema<Api.ReferenceRelation> | undefined>;
@@ -2,7 +2,7 @@ import * as Guards from './guards.js';
2
2
  /**
3
3
  * JSONPath parser that returns a relation that is guaranteed to have its schema populated.
4
4
  */
5
- export function findRelationByJSONPath(item, query) {
5
+ export async function findRelationByJSONPath(item, query, options = {}) {
6
6
  const tokens = parseJSONPath(query);
7
7
  const schemas = [];
8
8
  let current = item;
@@ -19,6 +19,16 @@ export function findRelationByJSONPath(item, query) {
19
19
  schemas.push(result['schema']);
20
20
  }
21
21
  current = result;
22
+ if (Guards.isRelationPointer(current) && options.resolveItem) {
23
+ const { itemPath, relationName } = current;
24
+ const item = await options.resolveItem(itemPath);
25
+ if (item) {
26
+ current = item.relations.find(relation => relation.name === relationName);
27
+ }
28
+ else {
29
+ return undefined;
30
+ }
31
+ }
22
32
  }
23
33
  if (Guards.isRelation(current, { strict: false })) {
24
34
  return current;
@@ -365,6 +365,12 @@ export interface Item<T extends RelationSchema | undefined = undefined> {
365
365
  */
366
366
  canonicalPath: string;
367
367
  }
368
+ /**
369
+ * Resolves a RelationPointer crossed by a schema-traversing helper (findRelationByJSONPath, findFieldByName,
370
+ * findValueByName) by fetching the pointed item. Each helper declares its own options object reusing this type,
371
+ * so a function-specific option can be added without touching the others.
372
+ */
373
+ export type ResolveItem = (itemPath: string) => Promise<Item | undefined>;
368
374
  /**
369
375
  * An ItemSummary is a constituent of a collection.
370
376
  * It links to an item, and can contain additional item information upon request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-api",
3
- "version": "8.0.9",
3
+ "version": "8.2.0",
4
4
  "description": "The Unito Integration API",
5
5
  "type": "module",
6
6
  "types": "./dist/src/index.d.ts",