@unito/integration-api 8.0.7 → 8.0.9
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/src/compatibilities.d.ts +1 -1
- package/dist/src/compatibilities.js +1 -1
- package/dist/src/fieldHelpers.d.ts +57 -0
- package/dist/src/fieldHelpers.js +308 -0
- package/dist/src/index.cjs +315 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types.d.ts +0 -3
- package/dist/src/types.js +0 -3
- package/package.json +1 -1
|
@@ -403,7 +403,7 @@ export const fieldTypeCompatibilityMatrix = {
|
|
|
403
403
|
[Api.FieldValueTypes.NUMBER]: {},
|
|
404
404
|
[Api.FieldValueTypes.OBJECT]: null,
|
|
405
405
|
[Api.FieldValueTypes.REFERENCE]: stringLikeToReferenceConfiguration,
|
|
406
|
-
[Api.FieldValueTypes.RICH_TEXT]:
|
|
406
|
+
[Api.FieldValueTypes.RICH_TEXT]: null,
|
|
407
407
|
[Api.FieldValueTypes.RICH_TEXT_HTML]: {},
|
|
408
408
|
[Api.FieldValueTypes.RICH_TEXT_MARKDOWN]: {},
|
|
409
409
|
[Api.FieldValueTypes.STRING]: {},
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { FieldSchema, FieldValues, Semantic } from './types.js';
|
|
2
|
+
declare const ValidFieldValueEntryBrand: unique symbol;
|
|
3
|
+
/**
|
|
4
|
+
* A single field value that has been validated against its FieldSchema.
|
|
5
|
+
*/
|
|
6
|
+
export type ValidFieldValueEntry<T> = T & {
|
|
7
|
+
[ValidFieldValueEntryBrand]: 'ValidFieldValueEntry';
|
|
8
|
+
};
|
|
9
|
+
declare const ValidFieldValueBrand: unique symbol;
|
|
10
|
+
/**
|
|
11
|
+
* A field value (single, array or null) that has been validated against its FieldSchema.
|
|
12
|
+
*/
|
|
13
|
+
export type ValidFieldValue<T> = (ValidFieldValueEntry<T> | ValidFieldValueEntry<T>[] | null) & {
|
|
14
|
+
[ValidFieldValueBrand]: 'ValidFieldValue';
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Escapes characters that would otherwise break dotted field name path parsing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sanitizeFieldName(fieldName: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Reverses sanitizeFieldName.
|
|
22
|
+
*/
|
|
23
|
+
export declare function unsanitizeFieldName(fieldName: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the validated value of a field, looking it up by name and falling back to its `semantic:` key.
|
|
26
|
+
*
|
|
27
|
+
* @throws Error if the value is invalid.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getUnsafeValue<T = unknown>(field: FieldSchema, values: FieldValues): ValidFieldValue<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Same as getUnsafeValue, but returns undefined instead of throwing on invalid values.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getSafeValue<T = unknown>(field: FieldSchema, values: FieldValues): ReturnType<typeof getUnsafeValue<T>> | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Returns a field value by its name, traversing dotted paths through OBJECT and REFERENCE fields.
|
|
36
|
+
*/
|
|
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
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns a field schema by its name, traversing dotted paths through OBJECT and REFERENCE fields.
|
|
46
|
+
* Also accepts `semantic:` prefixed names and sanitized names (see sanitizeFieldName).
|
|
47
|
+
*/
|
|
48
|
+
export declare function findFieldByName(fields: FieldSchema[], fieldName: string, inheritedProperties?: InheritedFieldProperties): FieldSchema | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Returns a field schema by its semantic.
|
|
51
|
+
*
|
|
52
|
+
* @throws Error if the found field does not respect the type constraints of its semantic.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findFieldBySemantic<T extends Semantic>(fields: FieldSchema[], semantic: T): Extract<FieldSchema, {
|
|
55
|
+
semantic: T;
|
|
56
|
+
}> | undefined;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { FieldValueTypes, Semantics } from './types.js';
|
|
2
|
+
import { isItemSummary, isObject, isRelationPointer } from './guards.js';
|
|
3
|
+
/**
|
|
4
|
+
* Escapes characters that would otherwise break dotted field name path parsing.
|
|
5
|
+
*/
|
|
6
|
+
export function sanitizeFieldName(fieldName) {
|
|
7
|
+
return fieldName.replaceAll('.', '__UNITO__DOT__').replaceAll('$', '__UNITO__DOLLAR__');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reverses sanitizeFieldName.
|
|
11
|
+
*/
|
|
12
|
+
export function unsanitizeFieldName(fieldName) {
|
|
13
|
+
return fieldName.replaceAll('__UNITO__DOT__', '.').replaceAll('__UNITO__DOLLAR__', '$');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates that a field schema received over the wire respects the type constraints of its semantic.
|
|
17
|
+
*/
|
|
18
|
+
function validateSemanticFieldSchema(field) {
|
|
19
|
+
if (field.semantic === Semantics.CREATED_AT || field.semantic === Semantics.UPDATED_AT) {
|
|
20
|
+
if (field.type !== FieldValueTypes.DATETIME) {
|
|
21
|
+
throw new Error(`Expected ${field.semantic} field to be a DATETIME, got ${field.type}`);
|
|
22
|
+
}
|
|
23
|
+
if (field.isArray) {
|
|
24
|
+
throw new Error(`Expected ${field.semantic} field to be a single value, got an array`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (field.semantic === Semantics.DISPLAY_NAME) {
|
|
28
|
+
if (field.type !== FieldValueTypes.STRING && field.type !== FieldValueTypes.EMAIL) {
|
|
29
|
+
throw new Error(`Expected displayName field to be a STRING or EMAIL, got ${field.type}`);
|
|
30
|
+
}
|
|
31
|
+
if (field.isArray) {
|
|
32
|
+
throw new Error(`Expected displayName field to be a single value, got an array`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (field.semantic === Semantics.PROVIDER_URL) {
|
|
36
|
+
if (field.type !== FieldValueTypes.URL) {
|
|
37
|
+
throw new Error(`Expected providerUrl field to be a URL, got ${field.type}`);
|
|
38
|
+
}
|
|
39
|
+
if (field.isArray) {
|
|
40
|
+
throw new Error(`Expected providerUrl field to be a single value, got an array`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (field.semantic === Semantics.DESCRIPTION) {
|
|
44
|
+
if (field.type !== FieldValueTypes.STRING &&
|
|
45
|
+
field.type !== FieldValueTypes.RICH_TEXT_HTML &&
|
|
46
|
+
field.type !== FieldValueTypes.RICH_TEXT_MARKDOWN &&
|
|
47
|
+
field.type !== FieldValueTypes.RICH_TEXT) {
|
|
48
|
+
throw new Error(`Expected description field to be a STRING, RICH_TEXT_HTML, RICH_TEXT_MARKDOWN or RICH_TEXT, got ${field.type}`);
|
|
49
|
+
}
|
|
50
|
+
if (field.isArray) {
|
|
51
|
+
throw new Error(`Expected description field to be a single value, got an array`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (field.semantic === Semantics.IS_PUBLIC && field.type !== FieldValueTypes.BOOLEAN) {
|
|
55
|
+
throw new Error(`Expected isPublic field to be a BOOLEAN, got ${field.type}`);
|
|
56
|
+
}
|
|
57
|
+
if ((field.semantic === Semantics.USER || field.semantic === Semantics.PARENT) &&
|
|
58
|
+
field.type !== FieldValueTypes.REFERENCE) {
|
|
59
|
+
throw new Error(`Expected ${field.semantic} field to be a REFERENCE, got ${field.type}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validates a value against a specific type.
|
|
64
|
+
*/
|
|
65
|
+
function validateFieldValueType(field, value) {
|
|
66
|
+
if (field.type === FieldValueTypes.STRING && typeof value !== 'string') {
|
|
67
|
+
throw new Error(`Expected ${field.name} to be a string, got ${typeof value}`);
|
|
68
|
+
}
|
|
69
|
+
if (field.type === FieldValueTypes.EMAIL) {
|
|
70
|
+
if (typeof value !== 'string') {
|
|
71
|
+
throw new Error(`Expected ${field.name} to be a string, got ${typeof value}`);
|
|
72
|
+
}
|
|
73
|
+
// Inspired by https://github.com/manishsaraan/email-validator/blob/master/index.js
|
|
74
|
+
const emailRegex = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
|
|
75
|
+
if (!emailRegex.test(value)) {
|
|
76
|
+
throw new Error(`Expected ${field.name} to be a valid email address`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (field.type === FieldValueTypes.URL) {
|
|
80
|
+
if (typeof value !== 'string') {
|
|
81
|
+
throw new Error(`Expected ${field.name} to be a string, got ${typeof value}`);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
new URL(value.startsWith('http') ? value : `http://${value}`);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new Error(`Expected ${field.name} to be a valid URL`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (field.type === FieldValueTypes.NUMBER && typeof value !== 'number') {
|
|
91
|
+
throw new Error(`Expected ${field.name} to be a number, got ${typeof value}`);
|
|
92
|
+
}
|
|
93
|
+
if (field.type === FieldValueTypes.INTEGER) {
|
|
94
|
+
if (typeof value !== 'number') {
|
|
95
|
+
throw new Error(`Expected ${field.name} to be a number, got ${typeof value}`);
|
|
96
|
+
}
|
|
97
|
+
if (!Number.isInteger(value)) {
|
|
98
|
+
throw new Error(`Expected ${field.name} to be an integer, got ${value}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (field.type === FieldValueTypes.BOOLEAN && typeof value !== 'boolean') {
|
|
102
|
+
throw new Error(`Expected ${field.name} to be a boolean, got ${typeof value}`);
|
|
103
|
+
}
|
|
104
|
+
if (field.type === FieldValueTypes.OBJECT && !isObject(value)) {
|
|
105
|
+
throw new Error(`Expected ${field.name} to be an object, got ${typeof value}`);
|
|
106
|
+
}
|
|
107
|
+
if (field.type === FieldValueTypes.REFERENCE && !isItemSummary(value)) {
|
|
108
|
+
throw new Error(`Expected ${field.name} to be an ItemSummary, got ${typeof value}`);
|
|
109
|
+
}
|
|
110
|
+
if (field.type === FieldValueTypes.DATETIME && (typeof value !== 'string' || isNaN(Date.parse(value)))) {
|
|
111
|
+
throw new Error(`Expected ${field.name} to be a valid ISO datetime string, got ${typeof value}`);
|
|
112
|
+
}
|
|
113
|
+
if (field.type === FieldValueTypes.DATE && (typeof value !== 'string' || isNaN(Date.parse(value)))) {
|
|
114
|
+
throw new Error(`Expected ${field.name} to be a valid ISO date string, got ${typeof value}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validates a field value against its schema.
|
|
119
|
+
*/
|
|
120
|
+
function validateFieldValue(field, value) {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
throw new Error(`Expected ${field.name} to be defined, got undefined`);
|
|
123
|
+
}
|
|
124
|
+
if (field.nullable === false && value === null) {
|
|
125
|
+
throw new Error(`Expected ${field.name} to be defined, got null`);
|
|
126
|
+
}
|
|
127
|
+
if (value === null) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (field.isArray && !Array.isArray(value)) {
|
|
131
|
+
throw new Error(`Expected ${field.name} to be an array, got ${typeof value}`);
|
|
132
|
+
}
|
|
133
|
+
if (!field.isArray && Array.isArray(value)) {
|
|
134
|
+
throw new Error(`Expected ${field.name} to be a single value, got an array`);
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
for (const subvalue of value) {
|
|
138
|
+
validateFieldValueType(field, subvalue);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
validateFieldValueType(field, value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns the validated value of a field, looking it up by name and falling back to its `semantic:` key.
|
|
147
|
+
*
|
|
148
|
+
* @throws Error if the value is invalid.
|
|
149
|
+
*/
|
|
150
|
+
export function getUnsafeValue(field, values) {
|
|
151
|
+
let value = values[field.name];
|
|
152
|
+
if (value === undefined) {
|
|
153
|
+
if (field.semantic && `semantic:${field.semantic}` in values) {
|
|
154
|
+
value = values[`semantic:${field.semantic}`];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
validateFieldValue(field, value);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Same as getUnsafeValue, but returns undefined instead of throwing on invalid values.
|
|
162
|
+
*/
|
|
163
|
+
export function getSafeValue(field, values) {
|
|
164
|
+
try {
|
|
165
|
+
return getUnsafeValue(field, values);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Returns a field value by its name, traversing dotted paths through OBJECT and REFERENCE fields.
|
|
173
|
+
*/
|
|
174
|
+
export function findValueByName(fields, values, fieldName) {
|
|
175
|
+
const [baseFieldName, ...subFieldNames] = fieldName.split('.');
|
|
176
|
+
if (baseFieldName === undefined) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const baseField = findFieldByName(fields, baseFieldName);
|
|
180
|
+
if (!baseField) {
|
|
181
|
+
if (subFieldNames.length === 0) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const literalField = findFieldByName(fields, fieldName);
|
|
185
|
+
return literalField ? getSafeValue(literalField, values) : undefined;
|
|
186
|
+
}
|
|
187
|
+
const baseFieldValue = getSafeValue(baseField, values);
|
|
188
|
+
if (subFieldNames.length === 0) {
|
|
189
|
+
return baseFieldValue;
|
|
190
|
+
}
|
|
191
|
+
if (baseField.type === FieldValueTypes.OBJECT) {
|
|
192
|
+
const subFieldName = subFieldNames.join('.');
|
|
193
|
+
if (isObject(baseFieldValue)) {
|
|
194
|
+
return findValueByName(baseField.fields, baseFieldValue, subFieldName);
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(baseFieldValue)) {
|
|
197
|
+
return baseFieldValue
|
|
198
|
+
.map(value => (isObject(value) ? findValueByName(baseField.fields, value, subFieldName) : undefined))
|
|
199
|
+
.filter(value => value !== undefined);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
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`);
|
|
205
|
+
}
|
|
206
|
+
if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
|
|
207
|
+
subFieldNames.shift();
|
|
208
|
+
}
|
|
209
|
+
const subFieldName = subFieldNames.join('.');
|
|
210
|
+
const referenceFields = baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields;
|
|
211
|
+
if (isItemSummary(baseFieldValue)) {
|
|
212
|
+
return findValueByName(referenceFields, baseFieldValue.fields ?? {}, subFieldName);
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(baseFieldValue)) {
|
|
215
|
+
return baseFieldValue
|
|
216
|
+
.map(value => isItemSummary(value) ? findValueByName(referenceFields, value.fields ?? {}, subFieldName) : undefined)
|
|
217
|
+
.filter(value => value !== undefined);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Subfields inherit some of their parents' properties: a subfield of an array field yields multiple
|
|
224
|
+
* values, and a subfield of a read-only field cannot be written to.
|
|
225
|
+
*/
|
|
226
|
+
function mergeInheritedProperties(field, inherited) {
|
|
227
|
+
const merged = { ...field };
|
|
228
|
+
const isArray = inherited.isArray || field.isArray;
|
|
229
|
+
if (isArray !== undefined) {
|
|
230
|
+
merged.isArray = isArray;
|
|
231
|
+
}
|
|
232
|
+
const canSetOnCreate = inherited.canSetOnCreate === false ? false : (field.canSetOnCreate ?? inherited.canSetOnCreate);
|
|
233
|
+
if (canSetOnCreate !== undefined) {
|
|
234
|
+
merged.canSetOnCreate = canSetOnCreate;
|
|
235
|
+
}
|
|
236
|
+
const canSetOnUpdate = inherited.canSetOnUpdate === false ? false : (field.canSetOnUpdate ?? inherited.canSetOnUpdate);
|
|
237
|
+
if (canSetOnUpdate !== undefined) {
|
|
238
|
+
merged.canSetOnUpdate = canSetOnUpdate;
|
|
239
|
+
}
|
|
240
|
+
const nullable = inherited.nullable || field.nullable;
|
|
241
|
+
if (nullable !== undefined) {
|
|
242
|
+
merged.nullable = nullable;
|
|
243
|
+
}
|
|
244
|
+
return merged;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Returns a field schema by its name, traversing dotted paths through OBJECT and REFERENCE fields.
|
|
248
|
+
* Also accepts `semantic:` prefixed names and sanitized names (see sanitizeFieldName).
|
|
249
|
+
*/
|
|
250
|
+
export function findFieldByName(fields, fieldName, inheritedProperties = {}) {
|
|
251
|
+
const [baseFieldName, ...subFieldNames] = fieldName.split('.');
|
|
252
|
+
if (baseFieldName === undefined) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
let baseField;
|
|
256
|
+
if (baseFieldName.startsWith('semantic:')) {
|
|
257
|
+
baseField = findFieldBySemantic(fields, baseFieldName.substring('semantic:'.length));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const unsanitizedBaseFieldName = unsanitizeFieldName(baseFieldName);
|
|
261
|
+
baseField = fields.find(field => field.name === unsanitizedBaseFieldName);
|
|
262
|
+
}
|
|
263
|
+
if (baseField) {
|
|
264
|
+
baseField = mergeInheritedProperties(baseField, inheritedProperties);
|
|
265
|
+
if (subFieldNames.length === 0) {
|
|
266
|
+
return baseField;
|
|
267
|
+
}
|
|
268
|
+
if (baseField.type === FieldValueTypes.OBJECT) {
|
|
269
|
+
return findFieldByName(baseField.fields, subFieldNames.join('.'), {
|
|
270
|
+
isArray: baseField.isArray,
|
|
271
|
+
canSetOnCreate: baseField.canSetOnCreate,
|
|
272
|
+
canSetOnUpdate: baseField.canSetOnUpdate,
|
|
273
|
+
nullable: baseField.nullable,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
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`);
|
|
279
|
+
}
|
|
280
|
+
if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
|
|
281
|
+
subFieldNames.shift();
|
|
282
|
+
}
|
|
283
|
+
return findFieldByName(baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields, subFieldNames.join('.'), {
|
|
284
|
+
isArray: baseField.isArray,
|
|
285
|
+
canSetOnCreate: baseField.canSetOnCreate,
|
|
286
|
+
canSetOnUpdate: baseField.canSetOnUpdate,
|
|
287
|
+
nullable: baseField.nullable,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
// If the dotted path lookup failed, try to find the field by its full name as is.
|
|
293
|
+
const unsanitizedFieldName = unsanitizeFieldName(fieldName);
|
|
294
|
+
return fields.find(field => field.name === unsanitizedFieldName);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Returns a field schema by its semantic.
|
|
298
|
+
*
|
|
299
|
+
* @throws Error if the found field does not respect the type constraints of its semantic.
|
|
300
|
+
*/
|
|
301
|
+
export function findFieldBySemantic(fields, semantic) {
|
|
302
|
+
const field = fields.find(field => field.semantic === semantic);
|
|
303
|
+
if (!field) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
validateSemanticFieldSchema(field);
|
|
307
|
+
return field;
|
|
308
|
+
}
|
package/dist/src/index.cjs
CHANGED
|
@@ -102,9 +102,6 @@ const StatusCodes = {
|
|
|
102
102
|
*/
|
|
103
103
|
const RelationSchemaDefaultValues = {
|
|
104
104
|
CAN_READ_ITEM: true,
|
|
105
|
-
CAN_CREATE_ITEM: true,
|
|
106
|
-
CAN_UPDATE_ITEM: true,
|
|
107
|
-
CAN_DELETE_ITEM: false,
|
|
108
105
|
CAN_ADD_FIELDS: false,
|
|
109
106
|
};
|
|
110
107
|
/**
|
|
@@ -682,7 +679,7 @@ const fieldTypeCompatibilityMatrix = {
|
|
|
682
679
|
[FieldValueTypes.NUMBER]: {},
|
|
683
680
|
[FieldValueTypes.OBJECT]: null,
|
|
684
681
|
[FieldValueTypes.REFERENCE]: stringLikeToReferenceConfiguration,
|
|
685
|
-
[FieldValueTypes.RICH_TEXT]:
|
|
682
|
+
[FieldValueTypes.RICH_TEXT]: null,
|
|
686
683
|
[FieldValueTypes.RICH_TEXT_HTML]: {},
|
|
687
684
|
[FieldValueTypes.RICH_TEXT_MARKDOWN]: {},
|
|
688
685
|
[FieldValueTypes.STRING]: {},
|
|
@@ -709,6 +706,313 @@ const fieldTypeCompatibilityMatrix = {
|
|
|
709
706
|
},
|
|
710
707
|
};
|
|
711
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Escapes characters that would otherwise break dotted field name path parsing.
|
|
711
|
+
*/
|
|
712
|
+
function sanitizeFieldName(fieldName) {
|
|
713
|
+
return fieldName.replaceAll('.', '__UNITO__DOT__').replaceAll('$', '__UNITO__DOLLAR__');
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Reverses sanitizeFieldName.
|
|
717
|
+
*/
|
|
718
|
+
function unsanitizeFieldName(fieldName) {
|
|
719
|
+
return fieldName.replaceAll('__UNITO__DOT__', '.').replaceAll('__UNITO__DOLLAR__', '$');
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Validates that a field schema received over the wire respects the type constraints of its semantic.
|
|
723
|
+
*/
|
|
724
|
+
function validateSemanticFieldSchema(field) {
|
|
725
|
+
if (field.semantic === Semantics.CREATED_AT || field.semantic === Semantics.UPDATED_AT) {
|
|
726
|
+
if (field.type !== FieldValueTypes.DATETIME) {
|
|
727
|
+
throw new Error(`Expected ${field.semantic} field to be a DATETIME, got ${field.type}`);
|
|
728
|
+
}
|
|
729
|
+
if (field.isArray) {
|
|
730
|
+
throw new Error(`Expected ${field.semantic} field to be a single value, got an array`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (field.semantic === Semantics.DISPLAY_NAME) {
|
|
734
|
+
if (field.type !== FieldValueTypes.STRING && field.type !== FieldValueTypes.EMAIL) {
|
|
735
|
+
throw new Error(`Expected displayName field to be a STRING or EMAIL, got ${field.type}`);
|
|
736
|
+
}
|
|
737
|
+
if (field.isArray) {
|
|
738
|
+
throw new Error(`Expected displayName field to be a single value, got an array`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (field.semantic === Semantics.PROVIDER_URL) {
|
|
742
|
+
if (field.type !== FieldValueTypes.URL) {
|
|
743
|
+
throw new Error(`Expected providerUrl field to be a URL, got ${field.type}`);
|
|
744
|
+
}
|
|
745
|
+
if (field.isArray) {
|
|
746
|
+
throw new Error(`Expected providerUrl field to be a single value, got an array`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (field.semantic === Semantics.DESCRIPTION) {
|
|
750
|
+
if (field.type !== FieldValueTypes.STRING &&
|
|
751
|
+
field.type !== FieldValueTypes.RICH_TEXT_HTML &&
|
|
752
|
+
field.type !== FieldValueTypes.RICH_TEXT_MARKDOWN &&
|
|
753
|
+
field.type !== FieldValueTypes.RICH_TEXT) {
|
|
754
|
+
throw new Error(`Expected description field to be a STRING, RICH_TEXT_HTML, RICH_TEXT_MARKDOWN or RICH_TEXT, got ${field.type}`);
|
|
755
|
+
}
|
|
756
|
+
if (field.isArray) {
|
|
757
|
+
throw new Error(`Expected description field to be a single value, got an array`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (field.semantic === Semantics.IS_PUBLIC && field.type !== FieldValueTypes.BOOLEAN) {
|
|
761
|
+
throw new Error(`Expected isPublic field to be a BOOLEAN, got ${field.type}`);
|
|
762
|
+
}
|
|
763
|
+
if ((field.semantic === Semantics.USER || field.semantic === Semantics.PARENT) &&
|
|
764
|
+
field.type !== FieldValueTypes.REFERENCE) {
|
|
765
|
+
throw new Error(`Expected ${field.semantic} field to be a REFERENCE, got ${field.type}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Validates a value against a specific type.
|
|
770
|
+
*/
|
|
771
|
+
function validateFieldValueType(field, value) {
|
|
772
|
+
if (field.type === FieldValueTypes.STRING && typeof value !== 'string') {
|
|
773
|
+
throw new Error(`Expected ${field.name} to be a string, got ${typeof value}`);
|
|
774
|
+
}
|
|
775
|
+
if (field.type === FieldValueTypes.EMAIL) {
|
|
776
|
+
if (typeof value !== 'string') {
|
|
777
|
+
throw new Error(`Expected ${field.name} to be a string, got ${typeof value}`);
|
|
778
|
+
}
|
|
779
|
+
// Inspired by https://github.com/manishsaraan/email-validator/blob/master/index.js
|
|
780
|
+
const emailRegex = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/;
|
|
781
|
+
if (!emailRegex.test(value)) {
|
|
782
|
+
throw new Error(`Expected ${field.name} to be a valid email address`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (field.type === FieldValueTypes.URL) {
|
|
786
|
+
if (typeof value !== 'string') {
|
|
787
|
+
throw new Error(`Expected ${field.name} to be a string, got ${typeof value}`);
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
new URL(value.startsWith('http') ? value : `http://${value}`);
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
throw new Error(`Expected ${field.name} to be a valid URL`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (field.type === FieldValueTypes.NUMBER && typeof value !== 'number') {
|
|
797
|
+
throw new Error(`Expected ${field.name} to be a number, got ${typeof value}`);
|
|
798
|
+
}
|
|
799
|
+
if (field.type === FieldValueTypes.INTEGER) {
|
|
800
|
+
if (typeof value !== 'number') {
|
|
801
|
+
throw new Error(`Expected ${field.name} to be a number, got ${typeof value}`);
|
|
802
|
+
}
|
|
803
|
+
if (!Number.isInteger(value)) {
|
|
804
|
+
throw new Error(`Expected ${field.name} to be an integer, got ${value}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (field.type === FieldValueTypes.BOOLEAN && typeof value !== 'boolean') {
|
|
808
|
+
throw new Error(`Expected ${field.name} to be a boolean, got ${typeof value}`);
|
|
809
|
+
}
|
|
810
|
+
if (field.type === FieldValueTypes.OBJECT && !isObject(value)) {
|
|
811
|
+
throw new Error(`Expected ${field.name} to be an object, got ${typeof value}`);
|
|
812
|
+
}
|
|
813
|
+
if (field.type === FieldValueTypes.REFERENCE && !isItemSummary(value)) {
|
|
814
|
+
throw new Error(`Expected ${field.name} to be an ItemSummary, got ${typeof value}`);
|
|
815
|
+
}
|
|
816
|
+
if (field.type === FieldValueTypes.DATETIME && (typeof value !== 'string' || isNaN(Date.parse(value)))) {
|
|
817
|
+
throw new Error(`Expected ${field.name} to be a valid ISO datetime string, got ${typeof value}`);
|
|
818
|
+
}
|
|
819
|
+
if (field.type === FieldValueTypes.DATE && (typeof value !== 'string' || isNaN(Date.parse(value)))) {
|
|
820
|
+
throw new Error(`Expected ${field.name} to be a valid ISO date string, got ${typeof value}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Validates a field value against its schema.
|
|
825
|
+
*/
|
|
826
|
+
function validateFieldValue(field, value) {
|
|
827
|
+
if (value === undefined) {
|
|
828
|
+
throw new Error(`Expected ${field.name} to be defined, got undefined`);
|
|
829
|
+
}
|
|
830
|
+
if (field.nullable === false && value === null) {
|
|
831
|
+
throw new Error(`Expected ${field.name} to be defined, got null`);
|
|
832
|
+
}
|
|
833
|
+
if (value === null) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (field.isArray && !Array.isArray(value)) {
|
|
837
|
+
throw new Error(`Expected ${field.name} to be an array, got ${typeof value}`);
|
|
838
|
+
}
|
|
839
|
+
if (!field.isArray && Array.isArray(value)) {
|
|
840
|
+
throw new Error(`Expected ${field.name} to be a single value, got an array`);
|
|
841
|
+
}
|
|
842
|
+
if (Array.isArray(value)) {
|
|
843
|
+
for (const subvalue of value) {
|
|
844
|
+
validateFieldValueType(field, subvalue);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
validateFieldValueType(field, value);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Returns the validated value of a field, looking it up by name and falling back to its `semantic:` key.
|
|
853
|
+
*
|
|
854
|
+
* @throws Error if the value is invalid.
|
|
855
|
+
*/
|
|
856
|
+
function getUnsafeValue(field, values) {
|
|
857
|
+
let value = values[field.name];
|
|
858
|
+
if (value === undefined) {
|
|
859
|
+
if (field.semantic && `semantic:${field.semantic}` in values) {
|
|
860
|
+
value = values[`semantic:${field.semantic}`];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
validateFieldValue(field, value);
|
|
864
|
+
return value;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Same as getUnsafeValue, but returns undefined instead of throwing on invalid values.
|
|
868
|
+
*/
|
|
869
|
+
function getSafeValue(field, values) {
|
|
870
|
+
try {
|
|
871
|
+
return getUnsafeValue(field, values);
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
return undefined;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Returns a field value by its name, traversing dotted paths through OBJECT and REFERENCE fields.
|
|
879
|
+
*/
|
|
880
|
+
function findValueByName(fields, values, fieldName) {
|
|
881
|
+
const [baseFieldName, ...subFieldNames] = fieldName.split('.');
|
|
882
|
+
if (baseFieldName === undefined) {
|
|
883
|
+
return undefined;
|
|
884
|
+
}
|
|
885
|
+
const baseField = findFieldByName(fields, baseFieldName);
|
|
886
|
+
if (!baseField) {
|
|
887
|
+
if (subFieldNames.length === 0) {
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
const literalField = findFieldByName(fields, fieldName);
|
|
891
|
+
return literalField ? getSafeValue(literalField, values) : undefined;
|
|
892
|
+
}
|
|
893
|
+
const baseFieldValue = getSafeValue(baseField, values);
|
|
894
|
+
if (subFieldNames.length === 0) {
|
|
895
|
+
return baseFieldValue;
|
|
896
|
+
}
|
|
897
|
+
if (baseField.type === FieldValueTypes.OBJECT) {
|
|
898
|
+
const subFieldName = subFieldNames.join('.');
|
|
899
|
+
if (isObject(baseFieldValue)) {
|
|
900
|
+
return findValueByName(baseField.fields, baseFieldValue, subFieldName);
|
|
901
|
+
}
|
|
902
|
+
if (Array.isArray(baseFieldValue)) {
|
|
903
|
+
return baseFieldValue
|
|
904
|
+
.map(value => (isObject(value) ? findValueByName(baseField.fields, value, subFieldName) : undefined))
|
|
905
|
+
.filter(value => value !== undefined);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
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`);
|
|
911
|
+
}
|
|
912
|
+
if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
|
|
913
|
+
subFieldNames.shift();
|
|
914
|
+
}
|
|
915
|
+
const subFieldName = subFieldNames.join('.');
|
|
916
|
+
const referenceFields = baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields;
|
|
917
|
+
if (isItemSummary(baseFieldValue)) {
|
|
918
|
+
return findValueByName(referenceFields, baseFieldValue.fields ?? {}, subFieldName);
|
|
919
|
+
}
|
|
920
|
+
if (Array.isArray(baseFieldValue)) {
|
|
921
|
+
return baseFieldValue
|
|
922
|
+
.map(value => isItemSummary(value) ? findValueByName(referenceFields, value.fields ?? {}, subFieldName) : undefined)
|
|
923
|
+
.filter(value => value !== undefined);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Subfields inherit some of their parents' properties: a subfield of an array field yields multiple
|
|
930
|
+
* values, and a subfield of a read-only field cannot be written to.
|
|
931
|
+
*/
|
|
932
|
+
function mergeInheritedProperties(field, inherited) {
|
|
933
|
+
const merged = { ...field };
|
|
934
|
+
const isArray = inherited.isArray || field.isArray;
|
|
935
|
+
if (isArray !== undefined) {
|
|
936
|
+
merged.isArray = isArray;
|
|
937
|
+
}
|
|
938
|
+
const canSetOnCreate = inherited.canSetOnCreate === false ? false : (field.canSetOnCreate ?? inherited.canSetOnCreate);
|
|
939
|
+
if (canSetOnCreate !== undefined) {
|
|
940
|
+
merged.canSetOnCreate = canSetOnCreate;
|
|
941
|
+
}
|
|
942
|
+
const canSetOnUpdate = inherited.canSetOnUpdate === false ? false : (field.canSetOnUpdate ?? inherited.canSetOnUpdate);
|
|
943
|
+
if (canSetOnUpdate !== undefined) {
|
|
944
|
+
merged.canSetOnUpdate = canSetOnUpdate;
|
|
945
|
+
}
|
|
946
|
+
const nullable = inherited.nullable || field.nullable;
|
|
947
|
+
if (nullable !== undefined) {
|
|
948
|
+
merged.nullable = nullable;
|
|
949
|
+
}
|
|
950
|
+
return merged;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Returns a field schema by its name, traversing dotted paths through OBJECT and REFERENCE fields.
|
|
954
|
+
* Also accepts `semantic:` prefixed names and sanitized names (see sanitizeFieldName).
|
|
955
|
+
*/
|
|
956
|
+
function findFieldByName(fields, fieldName, inheritedProperties = {}) {
|
|
957
|
+
const [baseFieldName, ...subFieldNames] = fieldName.split('.');
|
|
958
|
+
if (baseFieldName === undefined) {
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
let baseField;
|
|
962
|
+
if (baseFieldName.startsWith('semantic:')) {
|
|
963
|
+
baseField = findFieldBySemantic(fields, baseFieldName.substring('semantic:'.length));
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
const unsanitizedBaseFieldName = unsanitizeFieldName(baseFieldName);
|
|
967
|
+
baseField = fields.find(field => field.name === unsanitizedBaseFieldName);
|
|
968
|
+
}
|
|
969
|
+
if (baseField) {
|
|
970
|
+
baseField = mergeInheritedProperties(baseField, inheritedProperties);
|
|
971
|
+
if (subFieldNames.length === 0) {
|
|
972
|
+
return baseField;
|
|
973
|
+
}
|
|
974
|
+
if (baseField.type === FieldValueTypes.OBJECT) {
|
|
975
|
+
return findFieldByName(baseField.fields, subFieldNames.join('.'), {
|
|
976
|
+
isArray: baseField.isArray,
|
|
977
|
+
canSetOnCreate: baseField.canSetOnCreate,
|
|
978
|
+
canSetOnUpdate: baseField.canSetOnUpdate,
|
|
979
|
+
nullable: baseField.nullable,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
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`);
|
|
985
|
+
}
|
|
986
|
+
if (subFieldNames[0] === 'fields' && subFieldNames.length > 1) {
|
|
987
|
+
subFieldNames.shift();
|
|
988
|
+
}
|
|
989
|
+
return findFieldByName(baseField.reference.schema === '__self' ? fields : baseField.reference.schema.fields, subFieldNames.join('.'), {
|
|
990
|
+
isArray: baseField.isArray,
|
|
991
|
+
canSetOnCreate: baseField.canSetOnCreate,
|
|
992
|
+
canSetOnUpdate: baseField.canSetOnUpdate,
|
|
993
|
+
nullable: baseField.nullable,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
// If the dotted path lookup failed, try to find the field by its full name as is.
|
|
999
|
+
const unsanitizedFieldName = unsanitizeFieldName(fieldName);
|
|
1000
|
+
return fields.find(field => field.name === unsanitizedFieldName);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Returns a field schema by its semantic.
|
|
1004
|
+
*
|
|
1005
|
+
* @throws Error if the found field does not respect the type constraints of its semantic.
|
|
1006
|
+
*/
|
|
1007
|
+
function findFieldBySemantic(fields, semantic) {
|
|
1008
|
+
const field = fields.find(field => field.semantic === semantic);
|
|
1009
|
+
if (!field) {
|
|
1010
|
+
return undefined;
|
|
1011
|
+
}
|
|
1012
|
+
validateSemanticFieldSchema(field);
|
|
1013
|
+
return field;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
712
1016
|
/**
|
|
713
1017
|
* JSONPath parser that returns a relation that is guaranteed to have its schema populated.
|
|
714
1018
|
*/
|
|
@@ -937,7 +1241,12 @@ exports.RichTextNodeTypes = RichTextNodeTypes;
|
|
|
937
1241
|
exports.Semantics = Semantics;
|
|
938
1242
|
exports.StatusCodes = StatusCodes;
|
|
939
1243
|
exports.fieldTypeCompatibilityMatrix = fieldTypeCompatibilityMatrix;
|
|
1244
|
+
exports.findFieldByName = findFieldByName;
|
|
1245
|
+
exports.findFieldBySemantic = findFieldBySemantic;
|
|
940
1246
|
exports.findRelationByJSONPath = findRelationByJSONPath;
|
|
1247
|
+
exports.findValueByName = findValueByName;
|
|
1248
|
+
exports.getSafeValue = getSafeValue;
|
|
1249
|
+
exports.getUnsafeValue = getUnsafeValue;
|
|
941
1250
|
exports.isFieldSchema = isFieldSchema;
|
|
942
1251
|
exports.isFieldValueType = isFieldValueType;
|
|
943
1252
|
exports.isItem = isItem;
|
|
@@ -953,3 +1262,5 @@ exports.isRichTextNodeType = isRichTextNodeType;
|
|
|
953
1262
|
exports.isSemantic = isSemantic;
|
|
954
1263
|
exports.isString = isString;
|
|
955
1264
|
exports.isUndefined = isUndefined;
|
|
1265
|
+
exports.sanitizeFieldName = sanitizeFieldName;
|
|
1266
|
+
exports.unsanitizeFieldName = unsanitizeFieldName;
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
package/dist/src/types.d.ts
CHANGED
|
@@ -716,9 +716,6 @@ export type WebhookParseResponsePayload = WebhookParsedItem | WebhookItem | (Web
|
|
|
716
716
|
*/
|
|
717
717
|
export declare const RelationSchemaDefaultValues: {
|
|
718
718
|
readonly CAN_READ_ITEM: true;
|
|
719
|
-
readonly CAN_CREATE_ITEM: true;
|
|
720
|
-
readonly CAN_UPDATE_ITEM: true;
|
|
721
|
-
readonly CAN_DELETE_ITEM: false;
|
|
722
719
|
readonly CAN_ADD_FIELDS: false;
|
|
723
720
|
};
|
|
724
721
|
/**
|
package/dist/src/types.js
CHANGED