sg-widgets-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/client-BhHppzTV.d.mts +740 -0
- package/dist/client-DwN_-UuF.mjs +1015 -0
- package/dist/index.d.mts +2726 -0
- package/dist/index.mjs +7932 -0
- package/dist/mock.d.mts +167 -0
- package/dist/mock.mjs +3113 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
//#region src/field-types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Field data types and their filter vocabularies.
|
|
4
|
+
*
|
|
5
|
+
* Every list here is what a live Flow Production Tracking site answered when a
|
|
6
|
+
* probe sent it an unknown operator: the 400 names the legal set. Source:
|
|
7
|
+
* sg-groundtruth corpus, `findings/field_types/*.md` and `findings/017_filter_operators.md`.
|
|
8
|
+
*/
|
|
9
|
+
const DATA_TYPES = [
|
|
10
|
+
"text",
|
|
11
|
+
"number",
|
|
12
|
+
"float",
|
|
13
|
+
"percent",
|
|
14
|
+
"duration",
|
|
15
|
+
"timecode",
|
|
16
|
+
"currency",
|
|
17
|
+
"checkbox",
|
|
18
|
+
"date",
|
|
19
|
+
"date_time",
|
|
20
|
+
"list",
|
|
21
|
+
"status_list",
|
|
22
|
+
"entity",
|
|
23
|
+
"multi_entity",
|
|
24
|
+
"entity_type",
|
|
25
|
+
"color",
|
|
26
|
+
"image",
|
|
27
|
+
"url",
|
|
28
|
+
"jsonb",
|
|
29
|
+
"serializable",
|
|
30
|
+
"uuid",
|
|
31
|
+
"calculated",
|
|
32
|
+
"summary",
|
|
33
|
+
"password",
|
|
34
|
+
"pivot_column",
|
|
35
|
+
"footage",
|
|
36
|
+
"tag_list"
|
|
37
|
+
];
|
|
38
|
+
const OPERATORS = [
|
|
39
|
+
"is",
|
|
40
|
+
"is_not",
|
|
41
|
+
"in",
|
|
42
|
+
"not_in",
|
|
43
|
+
"contains",
|
|
44
|
+
"not_contains",
|
|
45
|
+
"starts_with",
|
|
46
|
+
"ends_with",
|
|
47
|
+
"greater_than",
|
|
48
|
+
"less_than",
|
|
49
|
+
"between",
|
|
50
|
+
"in_last",
|
|
51
|
+
"not_in_last",
|
|
52
|
+
"in_next",
|
|
53
|
+
"not_in_next",
|
|
54
|
+
"in_calendar_day",
|
|
55
|
+
"in_calendar_week",
|
|
56
|
+
"in_calendar_month",
|
|
57
|
+
"in_calendar_year",
|
|
58
|
+
"name_is",
|
|
59
|
+
"name_contains",
|
|
60
|
+
"name_not_contains",
|
|
61
|
+
"type_is",
|
|
62
|
+
"type_is_not"
|
|
63
|
+
];
|
|
64
|
+
const TIME_UNITS = [
|
|
65
|
+
"HOUR",
|
|
66
|
+
"DAY",
|
|
67
|
+
"WEEK",
|
|
68
|
+
"MONTH",
|
|
69
|
+
"YEAR"
|
|
70
|
+
];
|
|
71
|
+
const EQUALITY = [
|
|
72
|
+
"is",
|
|
73
|
+
"is_not",
|
|
74
|
+
"in",
|
|
75
|
+
"not_in"
|
|
76
|
+
];
|
|
77
|
+
const TEXT = [
|
|
78
|
+
"contains",
|
|
79
|
+
"not_contains",
|
|
80
|
+
"is",
|
|
81
|
+
"is_not",
|
|
82
|
+
"starts_with",
|
|
83
|
+
"ends_with",
|
|
84
|
+
"in",
|
|
85
|
+
"not_in"
|
|
86
|
+
];
|
|
87
|
+
const NUMERIC = [
|
|
88
|
+
"is",
|
|
89
|
+
"is_not",
|
|
90
|
+
"greater_than",
|
|
91
|
+
"less_than",
|
|
92
|
+
"between",
|
|
93
|
+
"in",
|
|
94
|
+
"not_in"
|
|
95
|
+
];
|
|
96
|
+
const TEMPORAL = [
|
|
97
|
+
"is",
|
|
98
|
+
"is_not",
|
|
99
|
+
"greater_than",
|
|
100
|
+
"less_than",
|
|
101
|
+
"in_last",
|
|
102
|
+
"not_in_last",
|
|
103
|
+
"in_next",
|
|
104
|
+
"not_in_next",
|
|
105
|
+
"in_calendar_week",
|
|
106
|
+
"in_calendar_month",
|
|
107
|
+
"in_calendar_day",
|
|
108
|
+
"in_calendar_year",
|
|
109
|
+
"between",
|
|
110
|
+
"in",
|
|
111
|
+
"not_in"
|
|
112
|
+
];
|
|
113
|
+
const LINK = [
|
|
114
|
+
"is",
|
|
115
|
+
"is_not",
|
|
116
|
+
"name_contains",
|
|
117
|
+
"name_not_contains",
|
|
118
|
+
"name_is",
|
|
119
|
+
"type_is",
|
|
120
|
+
"type_is_not",
|
|
121
|
+
"in",
|
|
122
|
+
"not_in"
|
|
123
|
+
];
|
|
124
|
+
const NONE = [];
|
|
125
|
+
/**
|
|
126
|
+
* Operators each data type accepts in a `_search` filter. An empty list means the
|
|
127
|
+
* type "cannot be used in a filter" and the API 400s on every operator.
|
|
128
|
+
*/
|
|
129
|
+
const OPERATORS_BY_TYPE = {
|
|
130
|
+
text: TEXT,
|
|
131
|
+
number: NUMERIC,
|
|
132
|
+
float: NUMERIC,
|
|
133
|
+
percent: NUMERIC,
|
|
134
|
+
duration: NUMERIC,
|
|
135
|
+
timecode: NUMERIC,
|
|
136
|
+
currency: NUMERIC,
|
|
137
|
+
footage: NUMERIC,
|
|
138
|
+
checkbox: ["is", "is_not"],
|
|
139
|
+
date: TEMPORAL,
|
|
140
|
+
date_time: TEMPORAL,
|
|
141
|
+
list: EQUALITY,
|
|
142
|
+
status_list: EQUALITY,
|
|
143
|
+
entity_type: EQUALITY,
|
|
144
|
+
color: EQUALITY,
|
|
145
|
+
uuid: EQUALITY,
|
|
146
|
+
entity: LINK,
|
|
147
|
+
multi_entity: LINK,
|
|
148
|
+
tag_list: LINK,
|
|
149
|
+
image: ["is", "is_not"],
|
|
150
|
+
jsonb: [
|
|
151
|
+
"is",
|
|
152
|
+
"is_not",
|
|
153
|
+
"contains",
|
|
154
|
+
"not_contains"
|
|
155
|
+
],
|
|
156
|
+
url: NONE,
|
|
157
|
+
serializable: NONE,
|
|
158
|
+
calculated: NONE,
|
|
159
|
+
summary: NONE,
|
|
160
|
+
password: NONE,
|
|
161
|
+
pivot_column: NONE
|
|
162
|
+
};
|
|
163
|
+
const VALUE_SHAPE = {
|
|
164
|
+
is: "scalar",
|
|
165
|
+
is_not: "scalar",
|
|
166
|
+
in: "list",
|
|
167
|
+
not_in: "list",
|
|
168
|
+
contains: "scalar",
|
|
169
|
+
not_contains: "scalar",
|
|
170
|
+
starts_with: "scalar",
|
|
171
|
+
ends_with: "scalar",
|
|
172
|
+
greater_than: "scalar",
|
|
173
|
+
less_than: "scalar",
|
|
174
|
+
between: "range",
|
|
175
|
+
in_last: "relative",
|
|
176
|
+
not_in_last: "relative",
|
|
177
|
+
in_next: "relative",
|
|
178
|
+
not_in_next: "relative",
|
|
179
|
+
in_calendar_day: "calendar",
|
|
180
|
+
in_calendar_week: "calendar",
|
|
181
|
+
in_calendar_month: "calendar",
|
|
182
|
+
in_calendar_year: "calendar",
|
|
183
|
+
name_is: "string",
|
|
184
|
+
name_contains: "string",
|
|
185
|
+
name_not_contains: "string",
|
|
186
|
+
type_is: "string",
|
|
187
|
+
type_is_not: "string"
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Operators whose result set includes rows where the field is null.
|
|
191
|
+
* `is_not X` is not the complement of `is X` on Flow PT: every negating operator
|
|
192
|
+
* matches unset rows, while comparisons exclude them.
|
|
193
|
+
*/
|
|
194
|
+
const NEGATING_OPERATORS = /* @__PURE__ */ new Set([
|
|
195
|
+
"is_not",
|
|
196
|
+
"not_in",
|
|
197
|
+
"not_contains",
|
|
198
|
+
"not_in_last",
|
|
199
|
+
"not_in_next",
|
|
200
|
+
"name_not_contains",
|
|
201
|
+
"type_is_not"
|
|
202
|
+
]);
|
|
203
|
+
function isDataType(value) {
|
|
204
|
+
return DATA_TYPES.includes(value);
|
|
205
|
+
}
|
|
206
|
+
function isOperator(value) {
|
|
207
|
+
return OPERATORS.includes(value);
|
|
208
|
+
}
|
|
209
|
+
/** Operators legal for a data type. Unknown types get the equality set, the smallest one the API prints. */
|
|
210
|
+
function operatorsFor(dataType) {
|
|
211
|
+
return isDataType(dataType) ? OPERATORS_BY_TYPE[dataType] : EQUALITY;
|
|
212
|
+
}
|
|
213
|
+
function isFilterable(dataType) {
|
|
214
|
+
return operatorsFor(dataType).length > 0;
|
|
215
|
+
}
|
|
216
|
+
function supportsOperator(dataType, operator) {
|
|
217
|
+
return operatorsFor(dataType).includes(operator);
|
|
218
|
+
}
|
|
219
|
+
/** Types whose scalar value is a `{type, id}` entity hash. */
|
|
220
|
+
function isLinkType(dataType) {
|
|
221
|
+
return dataType === "entity" || dataType === "multi_entity" || dataType === "tag_list";
|
|
222
|
+
}
|
|
223
|
+
/** Types whose scalar value is a number (integer unless noted). */
|
|
224
|
+
function isNumericType(dataType) {
|
|
225
|
+
return [
|
|
226
|
+
"number",
|
|
227
|
+
"float",
|
|
228
|
+
"percent",
|
|
229
|
+
"duration",
|
|
230
|
+
"timecode",
|
|
231
|
+
"currency",
|
|
232
|
+
"footage"
|
|
233
|
+
].includes(dataType);
|
|
234
|
+
}
|
|
235
|
+
function isTemporalType(dataType) {
|
|
236
|
+
return dataType === "date" || dataType === "date_time";
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/filter.ts
|
|
240
|
+
/**
|
|
241
|
+
* The filter array `entity_types` wants, `[]` for no filter
|
|
242
|
+
* (post_entity_text_search). The array form is `and` only and cannot express a
|
|
243
|
+
* nested group, so one is refused here rather than sent and guessed at.
|
|
244
|
+
*/
|
|
245
|
+
function toFilterArray(filter) {
|
|
246
|
+
if (!filter) return [];
|
|
247
|
+
if (Array.isArray(filter)) return filter;
|
|
248
|
+
if (filter.logical_operator !== "and") throw new Error("A '_text_search' filter is an array of conditions, which is 'and' only.");
|
|
249
|
+
return filter.conditions.map((c) => {
|
|
250
|
+
if (!Array.isArray(c)) throw new Error("A '_text_search' filter is an array of conditions and cannot nest a group.");
|
|
251
|
+
return c;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function group(logicalOperator, conditions = []) {
|
|
255
|
+
return {
|
|
256
|
+
kind: "group",
|
|
257
|
+
logicalOperator,
|
|
258
|
+
conditions
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function condition(path, operator, value) {
|
|
262
|
+
return {
|
|
263
|
+
kind: "condition",
|
|
264
|
+
path,
|
|
265
|
+
operator,
|
|
266
|
+
value
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function emptyFilter() {
|
|
270
|
+
return group("and");
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A condition is blank when it has no path, or when its operator needs a value
|
|
274
|
+
* and none was given. Blank conditions are dropped on serialisation rather than
|
|
275
|
+
* sent as a degenerate filter.
|
|
276
|
+
*/
|
|
277
|
+
function isBlankCondition(c) {
|
|
278
|
+
if (!c.path) return true;
|
|
279
|
+
const shape = VALUE_SHAPE[c.operator];
|
|
280
|
+
const v = c.value;
|
|
281
|
+
switch (shape) {
|
|
282
|
+
case "scalar":
|
|
283
|
+
case "string": return v === void 0 || v === "";
|
|
284
|
+
case "list": return !Array.isArray(v) || v.length === 0;
|
|
285
|
+
case "range": return !Array.isArray(v) || v.length !== 2 || v[0] == null || v[1] == null;
|
|
286
|
+
case "relative": return !Array.isArray(v) || v.length !== 2 || typeof v[0] !== "number" || v[0] <= 0 || !v[1];
|
|
287
|
+
case "calendar": return typeof v !== "number";
|
|
288
|
+
case "none": return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** True when serialisation would produce no conditions at all. */
|
|
292
|
+
function isEmptyFilter(node) {
|
|
293
|
+
if (node.kind === "condition") return isBlankCondition(node);
|
|
294
|
+
return node.conditions.every(isEmptyFilter);
|
|
295
|
+
}
|
|
296
|
+
function stripName(v) {
|
|
297
|
+
if (v !== null && typeof v === "object") return {
|
|
298
|
+
type: v.type,
|
|
299
|
+
id: v.id
|
|
300
|
+
};
|
|
301
|
+
return v;
|
|
302
|
+
}
|
|
303
|
+
function serializeValue(c) {
|
|
304
|
+
const shape = VALUE_SHAPE[c.operator];
|
|
305
|
+
const v = c.value;
|
|
306
|
+
if (shape === "list" || shape === "range") return v.map(stripName);
|
|
307
|
+
if (shape === "relative" || shape === "calendar" || shape === "none") return v;
|
|
308
|
+
return stripName(v);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Serialise a tree to the `api3_hash` `filters` value. Blank conditions and
|
|
312
|
+
* groups that end up empty are dropped. Returns `null` when nothing remains,
|
|
313
|
+
* so callers can omit `filters` rather than send `[]` (which matches every
|
|
314
|
+
* row on the site).
|
|
315
|
+
*/
|
|
316
|
+
function toApi3Hash(node) {
|
|
317
|
+
const out = serializeNode(node);
|
|
318
|
+
if (out === null) return null;
|
|
319
|
+
if (Array.isArray(out)) return {
|
|
320
|
+
logical_operator: "and",
|
|
321
|
+
conditions: [out]
|
|
322
|
+
};
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
function serializeNode(node) {
|
|
326
|
+
if (node.kind === "condition") {
|
|
327
|
+
if (isBlankCondition(node)) return null;
|
|
328
|
+
return [
|
|
329
|
+
node.path,
|
|
330
|
+
node.operator,
|
|
331
|
+
serializeValue(node)
|
|
332
|
+
];
|
|
333
|
+
}
|
|
334
|
+
const conditions = node.conditions.map(serializeNode).filter((c) => c !== null);
|
|
335
|
+
if (conditions.length === 0) return null;
|
|
336
|
+
return {
|
|
337
|
+
logical_operator: node.logicalOperator,
|
|
338
|
+
conditions
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/** Parse a wire group (or a flat `api3_array` list) back into the editor tree. */
|
|
342
|
+
function fromWire(wire) {
|
|
343
|
+
if (Array.isArray(wire)) return group("and", wire.map(fromWireCondition));
|
|
344
|
+
return group(wire.logical_operator, wire.conditions.map((c) => Array.isArray(c) ? fromWireCondition(c) : fromWire(c)));
|
|
345
|
+
}
|
|
346
|
+
function fromWireCondition([path, operator, value]) {
|
|
347
|
+
return condition(path, operator, value);
|
|
348
|
+
}
|
|
349
|
+
/** Depth-first walk, useful for collecting referenced paths or validating against a schema. */
|
|
350
|
+
function* walk(node) {
|
|
351
|
+
yield node;
|
|
352
|
+
if (node.kind === "group") for (const child of node.conditions) yield* walk(child);
|
|
353
|
+
}
|
|
354
|
+
function referencedPaths(node) {
|
|
355
|
+
const paths = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const n of walk(node)) if (n.kind === "condition" && n.path) paths.add(n.path);
|
|
357
|
+
return [...paths];
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/entity-path.ts
|
|
361
|
+
/** How a type is spelled in a url. Internal: nothing outside this package addresses the API by path. */
|
|
362
|
+
function pluralPath(entityType) {
|
|
363
|
+
const snake = entityType.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
364
|
+
if (snake.endsWith("s")) return `${snake}es`;
|
|
365
|
+
if (snake.endsWith("y") && !/[aeiou]y$/.test(snake)) return `${snake.slice(0, -1)}ies`;
|
|
366
|
+
return `${snake}s`;
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/schema.ts
|
|
370
|
+
/**
|
|
371
|
+
* Fields the schema types one way and the API answers another, keyed
|
|
372
|
+
* `<Type>.<field>`. `Note.read_by_current_user` holds the codes `unread` and
|
|
373
|
+
* `read`, never a boolean, so every control derived from it is a list and not a
|
|
374
|
+
* checkbox (067_notes_in_the_stream, entity_types/Note).
|
|
375
|
+
*
|
|
376
|
+
* An entry carries the whole field, because a site may leave it out of the schema
|
|
377
|
+
* and still answer it on the row: `GET /schema/Note/fields` answers 33 fields and
|
|
378
|
+
* no `read_by_current_user` among them, while `GET /schema/Note/fields/read_by_current_user`
|
|
379
|
+
* answers 200 with `data: null`, which is what an undeclared field reads as and a
|
|
380
|
+
* name that is nothing at all answers 404 (068_note_read_state). `normalizeFields`
|
|
381
|
+
* adds what the schema left out. A site that declares the field keeps its own
|
|
382
|
+
* display name and editability and is corrected on the data type, the values and
|
|
383
|
+
* the operators alone.
|
|
384
|
+
*
|
|
385
|
+
* `operators` is the set the API evaluates, which is not always the set it
|
|
386
|
+
* advertises. `read_by_current_user` names `is`, `is_not`, `in` and `not_in` in its
|
|
387
|
+
* own `Valid relations`, and evaluates only the first two: `in`, `not_in` and an
|
|
388
|
+
* `is` value outside the vocabulary all answer 200 with the caller's unread rows
|
|
389
|
+
* whatever the list holds, in `_search` and in `_summarize`, under both body shapes
|
|
390
|
+
* (068_note_read_state).
|
|
391
|
+
*
|
|
392
|
+
* The value is per person: a person's `PUT` answers 200 and the re-read follows it,
|
|
393
|
+
* so the field is editable. A script has no read state, and its own write answers
|
|
394
|
+
* 200 and stores nothing (068_note_read_state).
|
|
395
|
+
*/
|
|
396
|
+
const FIELD_SCHEMA_OVERRIDES = { "Note.read_by_current_user": {
|
|
397
|
+
displayName: "Read by Current User",
|
|
398
|
+
dataType: "list",
|
|
399
|
+
validValues: ["unread", "read"],
|
|
400
|
+
operators: ["is", "is_not"],
|
|
401
|
+
editable: true,
|
|
402
|
+
mandatory: false,
|
|
403
|
+
unique: false
|
|
404
|
+
} };
|
|
405
|
+
/** The part of an override that corrects a declaring site. */
|
|
406
|
+
const PATCHED_KEYS = [
|
|
407
|
+
"dataType",
|
|
408
|
+
"validValues",
|
|
409
|
+
"operators"
|
|
410
|
+
];
|
|
411
|
+
/** What a declared field's schema has to be corrected to, when it is one of those. */
|
|
412
|
+
function fieldSchemaOverride(entityType, name) {
|
|
413
|
+
const whole = FIELD_SCHEMA_OVERRIDES[`${entityType}.${name}`];
|
|
414
|
+
if (!whole) return void 0;
|
|
415
|
+
const patch = {};
|
|
416
|
+
for (const key of PATCHED_KEYS) if (whole[key] !== void 0) Object.assign(patch, { [key]: whole[key] });
|
|
417
|
+
return patch;
|
|
418
|
+
}
|
|
419
|
+
/** The whole field, for a site whose schema does not declare it; undefined when it is not one of those. */
|
|
420
|
+
function undeclaredField(entityType, name) {
|
|
421
|
+
const whole = FIELD_SCHEMA_OVERRIDES[`${entityType}.${name}`];
|
|
422
|
+
return whole ? {
|
|
423
|
+
name,
|
|
424
|
+
entityType,
|
|
425
|
+
...whole
|
|
426
|
+
} : void 0;
|
|
427
|
+
}
|
|
428
|
+
function prop(props, key) {
|
|
429
|
+
const p = props?.[key];
|
|
430
|
+
return p ? p.value : void 0;
|
|
431
|
+
}
|
|
432
|
+
function normalizeField(name, raw) {
|
|
433
|
+
const props = raw.properties;
|
|
434
|
+
const field = {
|
|
435
|
+
name,
|
|
436
|
+
displayName: raw.name.value,
|
|
437
|
+
entityType: raw.entity_type.value,
|
|
438
|
+
dataType: raw.data_type.value,
|
|
439
|
+
editable: raw.editable.value,
|
|
440
|
+
mandatory: raw.mandatory.value,
|
|
441
|
+
unique: raw.unique.value
|
|
442
|
+
};
|
|
443
|
+
const validTypes = prop(props, "valid_types");
|
|
444
|
+
if (validTypes) field.validTypes = validTypes;
|
|
445
|
+
const validValues = prop(props, "valid_values");
|
|
446
|
+
if (validValues) field.validValues = validValues;
|
|
447
|
+
const displayValues = prop(props, "display_values");
|
|
448
|
+
if (displayValues) field.displayValues = displayValues;
|
|
449
|
+
const hiddenValues = prop(props, "hidden_values");
|
|
450
|
+
if (hiddenValues) field.hiddenValues = hiddenValues;
|
|
451
|
+
const defaultValue = prop(props, "default_value");
|
|
452
|
+
if (defaultValue !== void 0) field.defaultValue = defaultValue;
|
|
453
|
+
const description = prop(props, "description");
|
|
454
|
+
if (description) field.description = description;
|
|
455
|
+
return {
|
|
456
|
+
...field,
|
|
457
|
+
...fieldSchemaOverride(field.entityType, name)
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Every field of a type, with the fields an override declares and the schema omitted.
|
|
462
|
+
*
|
|
463
|
+
* `entityType` is only needed for a response with no fields at all; otherwise the
|
|
464
|
+
* fields name their own type.
|
|
465
|
+
*/
|
|
466
|
+
function normalizeFields(response, entityType) {
|
|
467
|
+
const out = {};
|
|
468
|
+
for (const [name, raw] of Object.entries(response.data)) out[name] = normalizeField(name, raw);
|
|
469
|
+
const type = entityType ?? Object.values(out)[0]?.entityType;
|
|
470
|
+
if (!type) return out;
|
|
471
|
+
for (const key of Object.keys(FIELD_SCHEMA_OVERRIDES)) {
|
|
472
|
+
const at = key.indexOf(".");
|
|
473
|
+
const name = key.slice(at + 1);
|
|
474
|
+
if (key.slice(0, at) !== type || out[name]) continue;
|
|
475
|
+
const whole = undeclaredField(type, name);
|
|
476
|
+
if (whole) out[name] = whole;
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* The field name a type's own status lives under. Project's is `sg_status`, a plain
|
|
482
|
+
* `list` with the four values a bid goes through and no Status row behind it
|
|
483
|
+
* (probe 018, entity_types/Project).
|
|
484
|
+
*/
|
|
485
|
+
function statusFieldNameFor(entityType) {
|
|
486
|
+
return entityType === "Project" ? "sg_status" : "sg_status_list";
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The status field of an entity type. Every type but Project uses `sg_status_list`
|
|
490
|
+
* (data type `status_list`); Project's is `sg_status`, a plain `list`. Prefer the
|
|
491
|
+
* schema when you have it; the name guess is the fallback.
|
|
492
|
+
*
|
|
493
|
+
* The conventional name wins over any other `status_list` field on the type. A site
|
|
494
|
+
* is free to add its own — a client status, a delivery status — and a schema read
|
|
495
|
+
* answers them in no order the caller controls, so picking the first one found makes
|
|
496
|
+
* a type's status whichever field the response happened to list first.
|
|
497
|
+
*/
|
|
498
|
+
function statusFieldFor(entityType, fields) {
|
|
499
|
+
const name = statusFieldNameFor(entityType);
|
|
500
|
+
if (fields) {
|
|
501
|
+
const own = fields[name];
|
|
502
|
+
if (own) return own;
|
|
503
|
+
const status = Object.values(fields).find((f) => f.dataType === "status_list");
|
|
504
|
+
if (status) return status;
|
|
505
|
+
}
|
|
506
|
+
return name;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* The fields Flow PT conventionally uses as a row's human label, in order.
|
|
510
|
+
* `cached_display_name` is the server's own copy, the same string an entity dict
|
|
511
|
+
* carries as its `name`, and wins when present (060_entity_dict_name).
|
|
512
|
+
*/
|
|
513
|
+
const DISPLAY_NAME_FIELDS = [
|
|
514
|
+
"cached_display_name",
|
|
515
|
+
"code",
|
|
516
|
+
"name",
|
|
517
|
+
"title",
|
|
518
|
+
"content",
|
|
519
|
+
"subject"
|
|
520
|
+
];
|
|
521
|
+
function displayNameOf(attributes, fallback = "") {
|
|
522
|
+
if (!attributes) return fallback;
|
|
523
|
+
for (const key of DISPLAY_NAME_FIELDS) {
|
|
524
|
+
const v = attributes[key];
|
|
525
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
526
|
+
}
|
|
527
|
+
return fallback;
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/client.ts
|
|
531
|
+
/**
|
|
532
|
+
* Client adapter.
|
|
533
|
+
*
|
|
534
|
+
* Widgets talk to a `SgClient` and nothing else. Browser code rarely calls the
|
|
535
|
+
* Flow PT REST API directly (credentials, CORS), so most apps implement this
|
|
536
|
+
* against their own proxy. `RestClient` is the reference implementation against
|
|
537
|
+
* the REST API itself, for tests, tools and apps that can hold a token.
|
|
538
|
+
*/
|
|
539
|
+
/** The row a node stands for, or null when it stands for a type or nothing. */
|
|
540
|
+
function hierarchyEntity(ref) {
|
|
541
|
+
if (!ref || ref.kind !== "entity" || ref.value === null || typeof ref.value !== "object") return null;
|
|
542
|
+
return ref.value;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* The cut the event log takes, as the `and` group `_search` wants. Nothing here
|
|
546
|
+
* touches `meta`: it takes no filter (025_event_log).
|
|
547
|
+
*/
|
|
548
|
+
function eventLogFilters(options = {}) {
|
|
549
|
+
const conditions = [];
|
|
550
|
+
if (options.projectId !== void 0) conditions.push([
|
|
551
|
+
"project",
|
|
552
|
+
"is",
|
|
553
|
+
{
|
|
554
|
+
type: "Project",
|
|
555
|
+
id: options.projectId
|
|
556
|
+
}
|
|
557
|
+
]);
|
|
558
|
+
if (options.entity) conditions.push([
|
|
559
|
+
"entity",
|
|
560
|
+
"is",
|
|
561
|
+
{
|
|
562
|
+
type: options.entity.type,
|
|
563
|
+
id: options.entity.id
|
|
564
|
+
}
|
|
565
|
+
]);
|
|
566
|
+
if (options.eventType !== void 0) conditions.push(Array.isArray(options.eventType) ? [
|
|
567
|
+
"event_type",
|
|
568
|
+
"in",
|
|
569
|
+
options.eventType
|
|
570
|
+
] : [
|
|
571
|
+
"event_type",
|
|
572
|
+
"is",
|
|
573
|
+
options.eventType
|
|
574
|
+
]);
|
|
575
|
+
if (options.attributeName !== void 0) conditions.push([
|
|
576
|
+
"attribute_name",
|
|
577
|
+
"is",
|
|
578
|
+
options.attributeName
|
|
579
|
+
]);
|
|
580
|
+
if (options.since !== void 0) conditions.push([
|
|
581
|
+
"created_at",
|
|
582
|
+
"greater_than",
|
|
583
|
+
options.since
|
|
584
|
+
]);
|
|
585
|
+
if (options.until !== void 0) conditions.push([
|
|
586
|
+
"created_at",
|
|
587
|
+
"less_than",
|
|
588
|
+
options.until
|
|
589
|
+
]);
|
|
590
|
+
return {
|
|
591
|
+
logical_operator: "and",
|
|
592
|
+
conditions
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* What an event is read with. `audit_trail` is never returned even when it is
|
|
597
|
+
* named, so it is not asked for (025_event_log).
|
|
598
|
+
*/
|
|
599
|
+
const EVENT_LOG_FIELDS = [
|
|
600
|
+
"event_type",
|
|
601
|
+
"attribute_name",
|
|
602
|
+
"description",
|
|
603
|
+
"created_at",
|
|
604
|
+
"meta",
|
|
605
|
+
"entity",
|
|
606
|
+
"project",
|
|
607
|
+
"user"
|
|
608
|
+
];
|
|
609
|
+
/**
|
|
610
|
+
* Normalise one node and the level below it.
|
|
611
|
+
*
|
|
612
|
+
* The sample response gives a child a `label`, a `ref` and `has_children` but not
|
|
613
|
+
* always a `path`, so a child without one is addressed by appending its ref to the
|
|
614
|
+
* parent's path (post_hierarchy_expand).
|
|
615
|
+
*/
|
|
616
|
+
function normalizeHierarchyNode(raw, path) {
|
|
617
|
+
const ref = {
|
|
618
|
+
kind: String(raw.ref?.kind ?? "empty"),
|
|
619
|
+
value: raw.ref?.value ?? null
|
|
620
|
+
};
|
|
621
|
+
const own = raw.path ?? path;
|
|
622
|
+
return {
|
|
623
|
+
label: String(raw.label ?? ""),
|
|
624
|
+
ref,
|
|
625
|
+
path: own,
|
|
626
|
+
parentPath: raw.parent_path ?? null,
|
|
627
|
+
hasChildren: raw.has_children ?? false,
|
|
628
|
+
children: uniqueByPath((raw.children ?? []).map((child) => normalizeHierarchyNode(child, childPath(own, child))))
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Children keyed by path, first occurrence kept. `_expand` repeats the "no <field>"
|
|
633
|
+
* bucket (`.../Sequence/__none__`) once after every group on a grouped level, and the
|
|
634
|
+
* repeats are the same node (post_hierarchy_expand, 064_hierarchy_expand_buckets).
|
|
635
|
+
*/
|
|
636
|
+
function uniqueByPath(nodes) {
|
|
637
|
+
const seen = /* @__PURE__ */ new Set();
|
|
638
|
+
return nodes.filter((node) => seen.has(node.path) ? false : (seen.add(node.path), true));
|
|
639
|
+
}
|
|
640
|
+
function childPath(parentPath, child) {
|
|
641
|
+
if (typeof child.path === "string") return child.path;
|
|
642
|
+
const value = child.ref?.value;
|
|
643
|
+
if (child.ref?.kind === "entity_type" && typeof value === "string") return `${parentPath}/${value}`;
|
|
644
|
+
if (child.ref?.kind === "entity" && value !== null && typeof value === "object") return `${parentPath}/id/${value.id}`;
|
|
645
|
+
return parentPath;
|
|
646
|
+
}
|
|
647
|
+
var SgApiError = class extends Error {
|
|
648
|
+
status;
|
|
649
|
+
body;
|
|
650
|
+
constructor(status, body, message) {
|
|
651
|
+
super(message ?? `Flow PT API error ${status}`);
|
|
652
|
+
this.status = status;
|
|
653
|
+
this.body = body;
|
|
654
|
+
this.name = "SgApiError";
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
const API3_HASH = "application/vnd+shotgun.api3_hash+json";
|
|
658
|
+
function toHashGroup(filter) {
|
|
659
|
+
if (!filter) return {
|
|
660
|
+
logical_operator: "and",
|
|
661
|
+
conditions: []
|
|
662
|
+
};
|
|
663
|
+
if (Array.isArray(filter)) return {
|
|
664
|
+
logical_operator: "and",
|
|
665
|
+
conditions: filter
|
|
666
|
+
};
|
|
667
|
+
return filter;
|
|
668
|
+
}
|
|
669
|
+
var RestClient = class {
|
|
670
|
+
options;
|
|
671
|
+
base;
|
|
672
|
+
siteRoot;
|
|
673
|
+
fetchFn;
|
|
674
|
+
constructor(options) {
|
|
675
|
+
this.options = options;
|
|
676
|
+
this.siteRoot = options.siteUrl.replace(/\/+$/, "");
|
|
677
|
+
this.base = this.siteRoot + "/api/v1";
|
|
678
|
+
this.fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
679
|
+
}
|
|
680
|
+
async request(method, path, body, params, contentType = API3_HASH) {
|
|
681
|
+
const url = new URL(this.base + path);
|
|
682
|
+
for (const [k, v] of Object.entries(params ?? {})) if (v !== void 0) url.searchParams.set(k, String(v));
|
|
683
|
+
const headers = {
|
|
684
|
+
Accept: "application/json",
|
|
685
|
+
Authorization: `Bearer ${await this.options.token()}`
|
|
686
|
+
};
|
|
687
|
+
if (body !== void 0) headers["Content-Type"] = contentType;
|
|
688
|
+
const init = {
|
|
689
|
+
method,
|
|
690
|
+
headers
|
|
691
|
+
};
|
|
692
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
693
|
+
const res = await this.fetchFn(url, init);
|
|
694
|
+
const text = await res.text();
|
|
695
|
+
const json = text ? JSON.parse(text) : null;
|
|
696
|
+
if (!res.ok) {
|
|
697
|
+
const title = json?.errors?.[0]?.title;
|
|
698
|
+
throw new SgApiError(res.status, json, title);
|
|
699
|
+
}
|
|
700
|
+
return json;
|
|
701
|
+
}
|
|
702
|
+
async entityTypes() {
|
|
703
|
+
const res = await this.request("GET", "/schema");
|
|
704
|
+
return Object.entries(res.data).map(([name, v]) => ({
|
|
705
|
+
name,
|
|
706
|
+
displayName: v.name.value
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
async fields(entityType, projectId) {
|
|
710
|
+
return normalizeFields(await this.request("GET", `/schema/${entityType}/fields`, void 0, { project_id: projectId }), entityType);
|
|
711
|
+
}
|
|
712
|
+
async fieldWithProject(entityType, field, projectId) {
|
|
713
|
+
const res = await this.request("GET", `/schema/${entityType}/fields/${field}`, void 0, { project_id: projectId });
|
|
714
|
+
if (res.data) return normalizeField(field, res.data);
|
|
715
|
+
const known = undeclaredField(entityType, field);
|
|
716
|
+
if (known) return known;
|
|
717
|
+
throw new SgApiError(200, res, `Field '${entityType}.${field}' is not in the schema.`);
|
|
718
|
+
}
|
|
719
|
+
async search(entityType, options) {
|
|
720
|
+
const size = options.page?.size ?? 50;
|
|
721
|
+
const body = {
|
|
722
|
+
filters: options.filters ?? {
|
|
723
|
+
logical_operator: "and",
|
|
724
|
+
conditions: []
|
|
725
|
+
},
|
|
726
|
+
page: {
|
|
727
|
+
size,
|
|
728
|
+
number: options.page?.number ?? 1
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
if (options.fields) body["fields"] = options.fields.join(",");
|
|
732
|
+
if (options.sort) body["sort"] = options.sort;
|
|
733
|
+
const res = await this.request("POST", `/entity/${pluralPath(entityType)}/_search`, body);
|
|
734
|
+
return {
|
|
735
|
+
data: res.data,
|
|
736
|
+
hasMore: res.data.length === size
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
async textSearch(text, entityTypes, page) {
|
|
740
|
+
const types = {};
|
|
741
|
+
for (const [t, f] of Object.entries(entityTypes)) types[t] = toHashGroup(f);
|
|
742
|
+
const body = {
|
|
743
|
+
text,
|
|
744
|
+
entity_types: types,
|
|
745
|
+
page: {
|
|
746
|
+
size: Math.min(page?.size ?? 25, 25),
|
|
747
|
+
number: page?.number ?? 1
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
return (await this.request("POST", "/entity/_text_search", body)).data.map(toTextSearchRow);
|
|
751
|
+
}
|
|
752
|
+
async update(entityType, id, patch) {
|
|
753
|
+
return (await this.request("PUT", `/entity/${pluralPath(entityType)}/${id}`, patch, void 0, "application/json")).data;
|
|
754
|
+
}
|
|
755
|
+
async create(entityType, body) {
|
|
756
|
+
return (await this.request("POST", `/entity/${pluralPath(entityType)}`, body, void 0, "application/json")).data;
|
|
757
|
+
}
|
|
758
|
+
async upload(entityType, id, file) {
|
|
759
|
+
const field = file.field === void 0 ? "" : `/${file.field}`;
|
|
760
|
+
const ticket = await this.request("GET", `/entity/${pluralPath(entityType)}/${id}${field}/_upload`, void 0, { filename: file.filename });
|
|
761
|
+
const stored = await this.fetchFn(ticket.links.upload, {
|
|
762
|
+
method: "PUT",
|
|
763
|
+
body: file.data
|
|
764
|
+
});
|
|
765
|
+
if (!stored.ok) throw new SgApiError(stored.status, await stored.text(), "The presigned upload refused the bytes");
|
|
766
|
+
const completed = await this.fetchFn(new URL(ticket.links.complete_upload, this.siteRoot).toString(), {
|
|
767
|
+
method: "POST",
|
|
768
|
+
headers: {
|
|
769
|
+
Accept: "application/json",
|
|
770
|
+
Authorization: `Bearer ${await this.options.token()}`,
|
|
771
|
+
"Content-Type": "application/json"
|
|
772
|
+
},
|
|
773
|
+
body: JSON.stringify({
|
|
774
|
+
upload_info: ticket.data,
|
|
775
|
+
upload_data: {}
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
if (!completed.ok) {
|
|
779
|
+
const text = await completed.text();
|
|
780
|
+
let parsed = text;
|
|
781
|
+
try {
|
|
782
|
+
parsed = text ? JSON.parse(text) : null;
|
|
783
|
+
} catch {
|
|
784
|
+
parsed = text;
|
|
785
|
+
}
|
|
786
|
+
const title = parsed?.errors?.[0]?.title;
|
|
787
|
+
throw new SgApiError(completed.status, parsed, title);
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
uploadType: String(ticket.data["upload_type"] ?? ""),
|
|
791
|
+
uploadInfo: ticket.data,
|
|
792
|
+
etag: stored.headers.get("ETag")?.replace(/"/g, "") ?? null
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
async summarize(entityType, options = {}) {
|
|
796
|
+
const body = {
|
|
797
|
+
filters: options.filters ?? {
|
|
798
|
+
logical_operator: "and",
|
|
799
|
+
conditions: []
|
|
800
|
+
},
|
|
801
|
+
summary_fields: options.summaryFields ?? [{
|
|
802
|
+
field: "id",
|
|
803
|
+
type: "count"
|
|
804
|
+
}]
|
|
805
|
+
};
|
|
806
|
+
if (options.grouping) body["grouping"] = options.grouping.map((g) => ({
|
|
807
|
+
field: g.field,
|
|
808
|
+
type: g.type ?? "exact",
|
|
809
|
+
direction: g.direction ?? "asc"
|
|
810
|
+
}));
|
|
811
|
+
return normalizeSummarize(await this.request("POST", `/entity/${pluralPath(entityType)}/_summarize`, body));
|
|
812
|
+
}
|
|
813
|
+
async threadContents(noteId, entityFields) {
|
|
814
|
+
const params = {};
|
|
815
|
+
for (const [type, names] of Object.entries(entityFields ?? {})) params[`entity_fields[${type}]`] = names.join(",");
|
|
816
|
+
return (await this.request("GET", `/entity/notes/${noteId}/thread_contents`, void 0, params)).data.map(toThreadRow);
|
|
817
|
+
}
|
|
818
|
+
async eventLog(options = {}) {
|
|
819
|
+
const size = options.page?.size ?? 50;
|
|
820
|
+
const body = {
|
|
821
|
+
filters: eventLogFilters(options),
|
|
822
|
+
fields: EVENT_LOG_FIELDS.join(","),
|
|
823
|
+
sort: "-id",
|
|
824
|
+
page: {
|
|
825
|
+
size,
|
|
826
|
+
number: options.page?.number ?? 1
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
const res = await this.request("POST", "/entity/event_log_entries/_search", body);
|
|
830
|
+
return {
|
|
831
|
+
data: res.data.map(normalizeEventLogEntry),
|
|
832
|
+
hasMore: res.data.length === size
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
async following(userId, options = {}) {
|
|
836
|
+
return (await this.request("GET", `/entity/human_users/${userId}/following`, void 0, {
|
|
837
|
+
entity: options.entity,
|
|
838
|
+
project_id: options.projectId
|
|
839
|
+
})).data.map((row) => ({
|
|
840
|
+
type: String(row.type),
|
|
841
|
+
id: Number(row.id)
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
async hierarchyExpand(path) {
|
|
845
|
+
return normalizeHierarchyNode((await this.request("POST", "/hierarchy/_expand", { path }, void 0, "application/json")).data, path);
|
|
846
|
+
}
|
|
847
|
+
async hierarchySearch(rootPath, entity) {
|
|
848
|
+
const body = {
|
|
849
|
+
root_path: rootPath,
|
|
850
|
+
search_criteria: { entity: {
|
|
851
|
+
type: entity.type,
|
|
852
|
+
id: entity.id
|
|
853
|
+
} }
|
|
854
|
+
};
|
|
855
|
+
return (await this.request("POST", "/hierarchy/_search", body, void 0, "application/json")).data.map(toHierarchyPath);
|
|
856
|
+
}
|
|
857
|
+
async statuses() {
|
|
858
|
+
const res = await this.request("GET", "/entity/statuses", void 0, {
|
|
859
|
+
fields: "code,name,bg_color,icon",
|
|
860
|
+
"page[size]": 500
|
|
861
|
+
});
|
|
862
|
+
const iconIds = /* @__PURE__ */ new Set();
|
|
863
|
+
for (const row of res.data) {
|
|
864
|
+
const icon = row.relationships["icon"]?.data;
|
|
865
|
+
if (icon) iconIds.add(icon.id);
|
|
866
|
+
}
|
|
867
|
+
const icons = /* @__PURE__ */ new Map();
|
|
868
|
+
if (iconIds.size > 0) {
|
|
869
|
+
const iconRes = await this.request("POST", "/entity/icons/_search", {
|
|
870
|
+
filters: {
|
|
871
|
+
logical_operator: "and",
|
|
872
|
+
conditions: [[
|
|
873
|
+
"id",
|
|
874
|
+
"in",
|
|
875
|
+
[...iconIds]
|
|
876
|
+
]]
|
|
877
|
+
},
|
|
878
|
+
fields: "display_type,image_map_key,url,html,image_data",
|
|
879
|
+
page: {
|
|
880
|
+
size: 500,
|
|
881
|
+
number: 1
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
for (const i of iconRes.data) icons.set(i.id, i);
|
|
885
|
+
}
|
|
886
|
+
return res.data.map((row) => {
|
|
887
|
+
const iconRef = row.relationships["icon"]?.data;
|
|
888
|
+
const icon = iconRef ? icons.get(iconRef.id) : void 0;
|
|
889
|
+
return {
|
|
890
|
+
id: row.id,
|
|
891
|
+
code: String(row.attributes["code"] ?? ""),
|
|
892
|
+
name: String(row.attributes["name"] ?? ""),
|
|
893
|
+
bgColor: row.attributes["bg_color"] ?? null,
|
|
894
|
+
icon: icon ? toStatusIcon(icon.attributes) : null
|
|
895
|
+
};
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
/** A grouped call wraps the answer in `data`; an ungrouped one does not (020_summarize). */
|
|
900
|
+
function normalizeSummarize(res) {
|
|
901
|
+
const raw = res.data ?? res;
|
|
902
|
+
return {
|
|
903
|
+
summaries: raw.summaries ?? {},
|
|
904
|
+
groups: (raw.groups ?? []).map((g) => ({
|
|
905
|
+
groupName: String(g.group_name ?? ""),
|
|
906
|
+
groupValue: g.group_value ?? null,
|
|
907
|
+
summaries: g.summaries ?? {}
|
|
908
|
+
}))
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function toTextSearchRow(row) {
|
|
912
|
+
const links = row.attributes?.links;
|
|
913
|
+
const pair = Array.isArray(links) ? [String(links[0] ?? ""), String(links[1] ?? "")] : ["", ""];
|
|
914
|
+
return {
|
|
915
|
+
type: String(row.type),
|
|
916
|
+
id: Number(row.id),
|
|
917
|
+
name: String(row.attributes?.name ?? ""),
|
|
918
|
+
links: pair,
|
|
919
|
+
status: row.attributes?.status ?? null
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function toThreadAuthor(value) {
|
|
923
|
+
if (value === null || typeof value !== "object") return null;
|
|
924
|
+
const hash = value;
|
|
925
|
+
if (typeof hash.type !== "string" || typeof hash.id !== "number") return null;
|
|
926
|
+
const author = {
|
|
927
|
+
type: hash.type,
|
|
928
|
+
id: hash.id
|
|
929
|
+
};
|
|
930
|
+
if (typeof hash.name === "string") author.name = hash.name;
|
|
931
|
+
if (hash.image !== void 0) author.image = hash.image ?? null;
|
|
932
|
+
return author;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* The author key follows the row type: `created_by` on a Note and an Attachment,
|
|
936
|
+
* `user` on a Reply (get_entity_notes_id_thread_contents). A Note widened with
|
|
937
|
+
* `user` still names its author under `created_by`.
|
|
938
|
+
*/
|
|
939
|
+
function toThreadRow(row) {
|
|
940
|
+
const author = toThreadAuthor(row.type === "Reply" ? row["user"] : row["created_by"]);
|
|
941
|
+
return {
|
|
942
|
+
type: String(row.type),
|
|
943
|
+
id: Number(row.id),
|
|
944
|
+
createdAt: row["created_at"] ?? null,
|
|
945
|
+
content: row["content"] ?? null,
|
|
946
|
+
author,
|
|
947
|
+
fields: row
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function entityRefOf(value) {
|
|
951
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
952
|
+
const ref = value;
|
|
953
|
+
return typeof ref.name === "string" ? {
|
|
954
|
+
type: ref.type,
|
|
955
|
+
id: ref.id,
|
|
956
|
+
name: ref.name
|
|
957
|
+
} : {
|
|
958
|
+
type: ref.type,
|
|
959
|
+
id: ref.id
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/** One `_search` row of `EventLogEntry`, with `meta` decoded as the API returns it. */
|
|
963
|
+
function normalizeEventLogEntry(row) {
|
|
964
|
+
const a = row.attributes;
|
|
965
|
+
const meta = a["meta"] ?? null;
|
|
966
|
+
return {
|
|
967
|
+
id: row.id,
|
|
968
|
+
eventType: a["event_type"] ?? null,
|
|
969
|
+
attributeName: a["attribute_name"] ?? null,
|
|
970
|
+
description: a["description"] ?? null,
|
|
971
|
+
createdAt: a["created_at"] ?? null,
|
|
972
|
+
entity: entityRefOf(row.relationships["entity"]?.data),
|
|
973
|
+
project: entityRefOf(row.relationships["project"]?.data),
|
|
974
|
+
user: entityRefOf(row.relationships["user"]?.data),
|
|
975
|
+
meta,
|
|
976
|
+
oldValue: meta?.["old_value"] ?? null,
|
|
977
|
+
newValue: meta?.["new_value"] ?? null
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function toHierarchyPath(row) {
|
|
981
|
+
return {
|
|
982
|
+
label: String(row.label ?? ""),
|
|
983
|
+
pathLabel: String(row.path_label ?? ""),
|
|
984
|
+
incrementalPath: row.incremental_path ?? [],
|
|
985
|
+
ref: {
|
|
986
|
+
type: row.ref.type,
|
|
987
|
+
id: row.ref.id
|
|
988
|
+
},
|
|
989
|
+
projectId: row.project_id ?? null
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function toStatusIcon(a) {
|
|
993
|
+
switch (a["display_type"]) {
|
|
994
|
+
case "image_map": return {
|
|
995
|
+
displayType: "image_map",
|
|
996
|
+
imageMapKey: String(a["image_map_key"] ?? "")
|
|
997
|
+
};
|
|
998
|
+
case "image": {
|
|
999
|
+
const url = String(a["url"] ?? "").replace(/\s+/g, "");
|
|
1000
|
+
const data = String(a["image_data"] ?? "").replace(/\s+/g, "");
|
|
1001
|
+
const dataUrl = url || (data ? `data:image/png;base64,${data}` : "");
|
|
1002
|
+
return dataUrl ? {
|
|
1003
|
+
displayType: "image",
|
|
1004
|
+
dataUrl
|
|
1005
|
+
} : null;
|
|
1006
|
+
}
|
|
1007
|
+
case "html": return {
|
|
1008
|
+
displayType: "html",
|
|
1009
|
+
html: String(a["html"] ?? "")
|
|
1010
|
+
};
|
|
1011
|
+
default: return null;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
//#endregion
|
|
1015
|
+
export { OPERATORS as A, operatorsFor as B, isEmptyFilter as C, walk as D, toFilterArray as E, isFilterable as F, isLinkType as I, isNumericType as L, TIME_UNITS as M, VALUE_SHAPE as N, DATA_TYPES as O, isDataType as P, isOperator as R, isBlankCondition as S, toApi3Hash as T, supportsOperator as V, pluralPath as _, hierarchyEntity as a, fromWire as b, DISPLAY_NAME_FIELDS as c, fieldSchemaOverride as d, normalizeField as f, undeclaredField as g, statusFieldNameFor as h, eventLogFilters as i, OPERATORS_BY_TYPE as j, NEGATING_OPERATORS as k, FIELD_SCHEMA_OVERRIDES as l, statusFieldFor as m, RestClient as n, normalizeEventLogEntry as o, normalizeFields as p, SgApiError as r, normalizeHierarchyNode as s, EVENT_LOG_FIELDS as t, displayNameOf as u, condition as v, referencedPaths as w, group as x, emptyFilter as y, isTemporalType as z };
|