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
package/dist/mock.mjs
ADDED
|
@@ -0,0 +1,3113 @@
|
|
|
1
|
+
import { B as operatorsFor, E as toFilterArray, F as isFilterable, I as isLinkType, L as isNumericType, M as TIME_UNITS, _ as pluralPath, d as fieldSchemaOverride, i as eventLogFilters, k as NEGATING_OPERATORS, o as normalizeEventLogEntry, r as SgApiError, t as EVENT_LOG_FIELDS, u as displayNameOf } from "./client-DwN_-UuF.mjs";
|
|
2
|
+
//#region src/mock.ts
|
|
3
|
+
/**
|
|
4
|
+
* Status vocabularies. `valid_values` is the site's whole vocabulary and is
|
|
5
|
+
* byte-identical at every scope; only `hidden_values` varies per project
|
|
6
|
+
* (009_status_lists). Version's list is the one the probed site answered.
|
|
7
|
+
*/
|
|
8
|
+
const VERSION_STATUSES = [
|
|
9
|
+
"na",
|
|
10
|
+
"rev",
|
|
11
|
+
"vwd",
|
|
12
|
+
"apr",
|
|
13
|
+
"custom",
|
|
14
|
+
"fin",
|
|
15
|
+
"ip",
|
|
16
|
+
"clsd",
|
|
17
|
+
"cmpt",
|
|
18
|
+
"cfrm",
|
|
19
|
+
"pndad",
|
|
20
|
+
"pndl",
|
|
21
|
+
"pndvs",
|
|
22
|
+
"part",
|
|
23
|
+
"pass",
|
|
24
|
+
"pndng"
|
|
25
|
+
];
|
|
26
|
+
/** Task's list, also verbatim from 009_status_lists: it overlaps Version on five codes only. */
|
|
27
|
+
const TASK_STATUSES = [
|
|
28
|
+
"wtg",
|
|
29
|
+
"ip",
|
|
30
|
+
"fin",
|
|
31
|
+
"apr",
|
|
32
|
+
"dis",
|
|
33
|
+
"na",
|
|
34
|
+
"hld",
|
|
35
|
+
"rev",
|
|
36
|
+
"omt",
|
|
37
|
+
"ready"
|
|
38
|
+
];
|
|
39
|
+
const SHOT_STATUSES = [
|
|
40
|
+
"wtg",
|
|
41
|
+
"ip",
|
|
42
|
+
"rev",
|
|
43
|
+
"apr",
|
|
44
|
+
"fin",
|
|
45
|
+
"hld",
|
|
46
|
+
"omt"
|
|
47
|
+
];
|
|
48
|
+
/** Sequence's list, verbatim from entity_types/Sequence. */
|
|
49
|
+
const SEQUENCE_STATUSES = [
|
|
50
|
+
"wtg",
|
|
51
|
+
"ip",
|
|
52
|
+
"fin"
|
|
53
|
+
];
|
|
54
|
+
/** entity_types/HumanUser: two codes, `act` the default, and `act` is a condition on impersonation. */
|
|
55
|
+
const USER_STATUSES = ["act", "dis"];
|
|
56
|
+
/** A Note is created `opn`; the open set is what the `open_notes_count` rollups count (entity_types/Note). */
|
|
57
|
+
const NOTE_STATUSES = ["opn", "clsd"];
|
|
58
|
+
/** Attachment's own list on the probed site, `na` the default (entity_types/Attachment). */
|
|
59
|
+
const ATTACHMENT_STATUSES = ["fin", "na"];
|
|
60
|
+
const NOTE_TYPES = [
|
|
61
|
+
"Client",
|
|
62
|
+
"Internal",
|
|
63
|
+
"Direction"
|
|
64
|
+
];
|
|
65
|
+
/** `display_values` is per site, not per type; a missing key falls back to the raw code (009_status_lists). */
|
|
66
|
+
const STATUS_DISPLAY = {
|
|
67
|
+
na: "N/A",
|
|
68
|
+
rev: "Pending Review",
|
|
69
|
+
vwd: "Viewed",
|
|
70
|
+
apr: "Approved",
|
|
71
|
+
custom: "CustomIcon",
|
|
72
|
+
fin: "Final",
|
|
73
|
+
ip: "In Progress",
|
|
74
|
+
clsd: "Closed",
|
|
75
|
+
cmpt: "Complete",
|
|
76
|
+
cfrm: "Confirmed",
|
|
77
|
+
pndad: "Pending Art Director",
|
|
78
|
+
pndl: "Pending Lead",
|
|
79
|
+
pndvs: "Pending VFX Supervisor",
|
|
80
|
+
part: "partial",
|
|
81
|
+
pass: "pass",
|
|
82
|
+
pndng: "Pending",
|
|
83
|
+
wtg: "Waiting to Start",
|
|
84
|
+
hld: "On Hold",
|
|
85
|
+
omt: "Omitted",
|
|
86
|
+
dis: "Discarded",
|
|
87
|
+
ready: "Ready to Start",
|
|
88
|
+
act: "Active",
|
|
89
|
+
opn: "Open"
|
|
90
|
+
};
|
|
91
|
+
/** `bg_color` is comma-separated decimal RGB, never hex (010_status_icons). */
|
|
92
|
+
const STATUS_BG = {
|
|
93
|
+
na: "150,150,150",
|
|
94
|
+
wtg: "178,178,178",
|
|
95
|
+
ready: "120,190,240",
|
|
96
|
+
ip: "43,139,214",
|
|
97
|
+
rev: "250,190,53",
|
|
98
|
+
vwd: "154,113,190",
|
|
99
|
+
apr: "25,118,27",
|
|
100
|
+
fin: "80,143,66",
|
|
101
|
+
cmpt: "25,118,27",
|
|
102
|
+
cfrm: "52,152,219",
|
|
103
|
+
clsd: "90,90,90",
|
|
104
|
+
hld: "224,80,80",
|
|
105
|
+
omt: "128,128,128",
|
|
106
|
+
dis: "160,160,160",
|
|
107
|
+
part: "200,150,50",
|
|
108
|
+
pass: "100,180,100",
|
|
109
|
+
pndad: "236,151,31",
|
|
110
|
+
pndl: "236,151,31",
|
|
111
|
+
pndvs: "236,151,31",
|
|
112
|
+
pndng: "236,151,31",
|
|
113
|
+
custom: "255,105,180",
|
|
114
|
+
act: "25,118,27",
|
|
115
|
+
opn: "236,151,31"
|
|
116
|
+
};
|
|
117
|
+
/** A 1x1 png, standing in for the one `display_type: image` icon the probed site had. */
|
|
118
|
+
const TINY_PNG_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
119
|
+
const ASSET_TYPES = [
|
|
120
|
+
"Character",
|
|
121
|
+
"Environment",
|
|
122
|
+
"Prop",
|
|
123
|
+
"Vehicle",
|
|
124
|
+
"FX",
|
|
125
|
+
"Matte Painting"
|
|
126
|
+
];
|
|
127
|
+
const SHOT_TYPES = [
|
|
128
|
+
"VFX",
|
|
129
|
+
"2D",
|
|
130
|
+
"Full CG",
|
|
131
|
+
"Trailer",
|
|
132
|
+
"Marketing",
|
|
133
|
+
"Look Dev"
|
|
134
|
+
];
|
|
135
|
+
const VERSION_TYPES = [
|
|
136
|
+
"Type A",
|
|
137
|
+
"Type B",
|
|
138
|
+
"Type C"
|
|
139
|
+
];
|
|
140
|
+
/** Decimal `r,g,b`, the only form a colour field stores (field_types/color). */
|
|
141
|
+
const BAR_COLORS = [
|
|
142
|
+
"253,94,99",
|
|
143
|
+
"110,180,200",
|
|
144
|
+
"240,190,90",
|
|
145
|
+
"90,200,160"
|
|
146
|
+
];
|
|
147
|
+
/** Project's own status field is a plain `list` with no Status row behind it (entity_types/Project). */
|
|
148
|
+
const PROJECT_STATUSES = [
|
|
149
|
+
"Active",
|
|
150
|
+
"Bidding",
|
|
151
|
+
"Complete",
|
|
152
|
+
"On Hold"
|
|
153
|
+
];
|
|
154
|
+
const AUDIT = {
|
|
155
|
+
id: {
|
|
156
|
+
displayName: "Id",
|
|
157
|
+
dataType: "number",
|
|
158
|
+
editable: false
|
|
159
|
+
},
|
|
160
|
+
cached_display_name: {
|
|
161
|
+
displayName: "Display Name",
|
|
162
|
+
dataType: "text"
|
|
163
|
+
},
|
|
164
|
+
created_at: {
|
|
165
|
+
displayName: "Date Created",
|
|
166
|
+
dataType: "date_time",
|
|
167
|
+
editable: false,
|
|
168
|
+
createOnly: true
|
|
169
|
+
},
|
|
170
|
+
updated_at: {
|
|
171
|
+
displayName: "Date Updated",
|
|
172
|
+
dataType: "date_time",
|
|
173
|
+
editable: false,
|
|
174
|
+
createOnly: true
|
|
175
|
+
},
|
|
176
|
+
created_by: {
|
|
177
|
+
displayName: "Created by",
|
|
178
|
+
dataType: "entity",
|
|
179
|
+
editable: false,
|
|
180
|
+
validTypes: ["HumanUser", "ApiUser"]
|
|
181
|
+
},
|
|
182
|
+
updated_by: {
|
|
183
|
+
displayName: "Updated by",
|
|
184
|
+
dataType: "entity",
|
|
185
|
+
editable: false,
|
|
186
|
+
validTypes: ["HumanUser", "ApiUser"]
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
function statusSpec(validValues, defaultValue, mandatory = false) {
|
|
190
|
+
return {
|
|
191
|
+
displayName: "Status",
|
|
192
|
+
dataType: "status_list",
|
|
193
|
+
validValues,
|
|
194
|
+
displayValues: Object.fromEntries(validValues.map((c) => [c, STATUS_DISPLAY[c] ?? c])),
|
|
195
|
+
defaultValue,
|
|
196
|
+
mandatory
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const SPECS = {
|
|
200
|
+
Project: {
|
|
201
|
+
...AUDIT,
|
|
202
|
+
name: {
|
|
203
|
+
displayName: "Project Name",
|
|
204
|
+
dataType: "text",
|
|
205
|
+
mandatory: true,
|
|
206
|
+
unique: true
|
|
207
|
+
},
|
|
208
|
+
code: {
|
|
209
|
+
displayName: "Project Code",
|
|
210
|
+
dataType: "text",
|
|
211
|
+
unique: true
|
|
212
|
+
},
|
|
213
|
+
tank_name: {
|
|
214
|
+
displayName: "Tank Name",
|
|
215
|
+
dataType: "text"
|
|
216
|
+
},
|
|
217
|
+
sg_status: {
|
|
218
|
+
displayName: "Status",
|
|
219
|
+
dataType: "list",
|
|
220
|
+
validValues: PROJECT_STATUSES,
|
|
221
|
+
defaultValue: "Active"
|
|
222
|
+
},
|
|
223
|
+
sg_type: {
|
|
224
|
+
displayName: "Type",
|
|
225
|
+
dataType: "list",
|
|
226
|
+
validValues: [
|
|
227
|
+
"Feature",
|
|
228
|
+
"Episodic",
|
|
229
|
+
"Commercial",
|
|
230
|
+
"Short"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
sg_description: {
|
|
234
|
+
displayName: "Description",
|
|
235
|
+
dataType: "text"
|
|
236
|
+
},
|
|
237
|
+
sg_start_date: {
|
|
238
|
+
displayName: "Start Date",
|
|
239
|
+
dataType: "date"
|
|
240
|
+
},
|
|
241
|
+
sg_end_date: {
|
|
242
|
+
displayName: "End Date",
|
|
243
|
+
dataType: "date"
|
|
244
|
+
},
|
|
245
|
+
sg_frame_rate: {
|
|
246
|
+
displayName: "Frame Rate",
|
|
247
|
+
dataType: "float"
|
|
248
|
+
},
|
|
249
|
+
sg_progress: {
|
|
250
|
+
displayName: "Progress",
|
|
251
|
+
dataType: "percent"
|
|
252
|
+
},
|
|
253
|
+
archived: {
|
|
254
|
+
displayName: "Archived",
|
|
255
|
+
dataType: "checkbox"
|
|
256
|
+
},
|
|
257
|
+
is_demo: {
|
|
258
|
+
displayName: "Is Demo",
|
|
259
|
+
dataType: "checkbox",
|
|
260
|
+
editable: false
|
|
261
|
+
},
|
|
262
|
+
is_template: {
|
|
263
|
+
displayName: "Is Template",
|
|
264
|
+
dataType: "checkbox",
|
|
265
|
+
editable: false
|
|
266
|
+
},
|
|
267
|
+
image: {
|
|
268
|
+
displayName: "Thumbnail",
|
|
269
|
+
dataType: "image"
|
|
270
|
+
},
|
|
271
|
+
landing_page_url: {
|
|
272
|
+
displayName: "Landing Page URL",
|
|
273
|
+
dataType: "text",
|
|
274
|
+
editable: false
|
|
275
|
+
},
|
|
276
|
+
users: {
|
|
277
|
+
displayName: "Users",
|
|
278
|
+
dataType: "multi_entity",
|
|
279
|
+
validTypes: ["HumanUser"]
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
Sequence: {
|
|
283
|
+
...AUDIT,
|
|
284
|
+
code: {
|
|
285
|
+
displayName: "Sequence Name",
|
|
286
|
+
dataType: "text",
|
|
287
|
+
mandatory: true
|
|
288
|
+
},
|
|
289
|
+
description: {
|
|
290
|
+
displayName: "Description",
|
|
291
|
+
dataType: "text"
|
|
292
|
+
},
|
|
293
|
+
sg_status_list: statusSpec(SEQUENCE_STATUSES, "ip"),
|
|
294
|
+
sg_timecode: {
|
|
295
|
+
displayName: "Timecode",
|
|
296
|
+
dataType: "timecode"
|
|
297
|
+
},
|
|
298
|
+
sg_cut_duration: {
|
|
299
|
+
displayName: "Cut Duration",
|
|
300
|
+
dataType: "number"
|
|
301
|
+
},
|
|
302
|
+
image: {
|
|
303
|
+
displayName: "Thumbnail",
|
|
304
|
+
dataType: "image"
|
|
305
|
+
},
|
|
306
|
+
project: {
|
|
307
|
+
displayName: "Project",
|
|
308
|
+
dataType: "entity",
|
|
309
|
+
mandatory: true,
|
|
310
|
+
validTypes: ["Project"]
|
|
311
|
+
},
|
|
312
|
+
shots: {
|
|
313
|
+
displayName: "Shots",
|
|
314
|
+
dataType: "multi_entity",
|
|
315
|
+
validTypes: ["Shot"]
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
Shot: {
|
|
319
|
+
...AUDIT,
|
|
320
|
+
code: {
|
|
321
|
+
displayName: "Shot Code",
|
|
322
|
+
dataType: "text",
|
|
323
|
+
mandatory: true
|
|
324
|
+
},
|
|
325
|
+
description: {
|
|
326
|
+
displayName: "Description",
|
|
327
|
+
dataType: "text"
|
|
328
|
+
},
|
|
329
|
+
sg_status_list: statusSpec(SHOT_STATUSES, "wtg"),
|
|
330
|
+
sg_shot_type: {
|
|
331
|
+
displayName: "Shot Type",
|
|
332
|
+
dataType: "list",
|
|
333
|
+
validValues: SHOT_TYPES
|
|
334
|
+
},
|
|
335
|
+
sg_cut_in: {
|
|
336
|
+
displayName: "Cut In",
|
|
337
|
+
dataType: "number"
|
|
338
|
+
},
|
|
339
|
+
sg_cut_out: {
|
|
340
|
+
displayName: "Cut Out",
|
|
341
|
+
dataType: "number"
|
|
342
|
+
},
|
|
343
|
+
sg_cut_duration: {
|
|
344
|
+
displayName: "Cut Duration",
|
|
345
|
+
dataType: "number"
|
|
346
|
+
},
|
|
347
|
+
sg_working_duration: {
|
|
348
|
+
displayName: "Working Duration",
|
|
349
|
+
dataType: "duration"
|
|
350
|
+
},
|
|
351
|
+
sg_turnover_date: {
|
|
352
|
+
displayName: "Turnover Date",
|
|
353
|
+
dataType: "date"
|
|
354
|
+
},
|
|
355
|
+
sg_complexity: {
|
|
356
|
+
displayName: "Complexity",
|
|
357
|
+
dataType: "percent"
|
|
358
|
+
},
|
|
359
|
+
sg_lens: {
|
|
360
|
+
displayName: "Lens (mm)",
|
|
361
|
+
dataType: "float"
|
|
362
|
+
},
|
|
363
|
+
sg_omit: {
|
|
364
|
+
displayName: "Omitted",
|
|
365
|
+
dataType: "checkbox"
|
|
366
|
+
},
|
|
367
|
+
image: {
|
|
368
|
+
displayName: "Thumbnail",
|
|
369
|
+
dataType: "image"
|
|
370
|
+
},
|
|
371
|
+
sg_shot_notes_url: {
|
|
372
|
+
displayName: "Notes URL",
|
|
373
|
+
dataType: "url"
|
|
374
|
+
},
|
|
375
|
+
project: {
|
|
376
|
+
displayName: "Project",
|
|
377
|
+
dataType: "entity",
|
|
378
|
+
mandatory: true,
|
|
379
|
+
validTypes: ["Project"]
|
|
380
|
+
},
|
|
381
|
+
sg_sequence: {
|
|
382
|
+
displayName: "Sequence",
|
|
383
|
+
dataType: "entity",
|
|
384
|
+
validTypes: ["Sequence"]
|
|
385
|
+
},
|
|
386
|
+
assets: {
|
|
387
|
+
displayName: "Assets",
|
|
388
|
+
dataType: "multi_entity",
|
|
389
|
+
validTypes: ["Asset"]
|
|
390
|
+
},
|
|
391
|
+
tasks: {
|
|
392
|
+
displayName: "Tasks",
|
|
393
|
+
dataType: "multi_entity",
|
|
394
|
+
validTypes: ["Task"]
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
Asset: {
|
|
398
|
+
...AUDIT,
|
|
399
|
+
code: {
|
|
400
|
+
displayName: "Asset Name",
|
|
401
|
+
dataType: "text",
|
|
402
|
+
mandatory: true
|
|
403
|
+
},
|
|
404
|
+
description: {
|
|
405
|
+
displayName: "Description",
|
|
406
|
+
dataType: "text"
|
|
407
|
+
},
|
|
408
|
+
sg_status_list: statusSpec(SHOT_STATUSES, "wtg"),
|
|
409
|
+
sg_asset_type: {
|
|
410
|
+
displayName: "Asset Type",
|
|
411
|
+
dataType: "list",
|
|
412
|
+
validValues: ASSET_TYPES
|
|
413
|
+
},
|
|
414
|
+
sg_build_days: {
|
|
415
|
+
displayName: "Build Days",
|
|
416
|
+
dataType: "number"
|
|
417
|
+
},
|
|
418
|
+
sg_complexity: {
|
|
419
|
+
displayName: "Complexity",
|
|
420
|
+
dataType: "percent"
|
|
421
|
+
},
|
|
422
|
+
sg_due_date: {
|
|
423
|
+
displayName: "Due Date",
|
|
424
|
+
dataType: "date"
|
|
425
|
+
},
|
|
426
|
+
sg_published: {
|
|
427
|
+
displayName: "Published",
|
|
428
|
+
dataType: "checkbox"
|
|
429
|
+
},
|
|
430
|
+
image: {
|
|
431
|
+
displayName: "Thumbnail",
|
|
432
|
+
dataType: "image"
|
|
433
|
+
},
|
|
434
|
+
project: {
|
|
435
|
+
displayName: "Project",
|
|
436
|
+
dataType: "entity",
|
|
437
|
+
mandatory: true,
|
|
438
|
+
validTypes: ["Project"]
|
|
439
|
+
},
|
|
440
|
+
shots: {
|
|
441
|
+
displayName: "Shots",
|
|
442
|
+
dataType: "multi_entity",
|
|
443
|
+
validTypes: ["Shot"]
|
|
444
|
+
},
|
|
445
|
+
sequences: {
|
|
446
|
+
displayName: "Sequences",
|
|
447
|
+
dataType: "multi_entity",
|
|
448
|
+
validTypes: ["Sequence"]
|
|
449
|
+
},
|
|
450
|
+
tasks: {
|
|
451
|
+
displayName: "Tasks",
|
|
452
|
+
dataType: "multi_entity",
|
|
453
|
+
validTypes: ["Task"]
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
Version: {
|
|
457
|
+
...AUDIT,
|
|
458
|
+
code: {
|
|
459
|
+
displayName: "Version Name",
|
|
460
|
+
dataType: "text",
|
|
461
|
+
mandatory: true
|
|
462
|
+
},
|
|
463
|
+
description: {
|
|
464
|
+
displayName: "Description",
|
|
465
|
+
dataType: "text"
|
|
466
|
+
},
|
|
467
|
+
sg_department: {
|
|
468
|
+
displayName: "Department",
|
|
469
|
+
dataType: "text"
|
|
470
|
+
},
|
|
471
|
+
sg_status_list: statusSpec(VERSION_STATUSES, "rev"),
|
|
472
|
+
sg_version_type: {
|
|
473
|
+
displayName: "Version Type",
|
|
474
|
+
dataType: "list",
|
|
475
|
+
validValues: VERSION_TYPES,
|
|
476
|
+
defaultValue: "Type A"
|
|
477
|
+
},
|
|
478
|
+
sg_first_frame: {
|
|
479
|
+
displayName: "First Frame",
|
|
480
|
+
dataType: "number"
|
|
481
|
+
},
|
|
482
|
+
sg_last_frame: {
|
|
483
|
+
displayName: "Last Frame",
|
|
484
|
+
dataType: "number"
|
|
485
|
+
},
|
|
486
|
+
frame_count: {
|
|
487
|
+
displayName: "Frame Count",
|
|
488
|
+
dataType: "number"
|
|
489
|
+
},
|
|
490
|
+
sg_uploaded_movie_frame_rate: {
|
|
491
|
+
displayName: "Movie Frame Rate",
|
|
492
|
+
dataType: "float"
|
|
493
|
+
},
|
|
494
|
+
sg_uploaded_movie_transcoding_status: {
|
|
495
|
+
displayName: "Transcoding Status",
|
|
496
|
+
dataType: "number"
|
|
497
|
+
},
|
|
498
|
+
sg_path_to_frames: {
|
|
499
|
+
displayName: "Path to Frames",
|
|
500
|
+
dataType: "text"
|
|
501
|
+
},
|
|
502
|
+
sg_path_to_movie: {
|
|
503
|
+
displayName: "Path to Movie",
|
|
504
|
+
dataType: "text"
|
|
505
|
+
},
|
|
506
|
+
client_approved: {
|
|
507
|
+
displayName: "Client Approved",
|
|
508
|
+
dataType: "checkbox"
|
|
509
|
+
},
|
|
510
|
+
client_approved_at: {
|
|
511
|
+
displayName: "Client Approved at",
|
|
512
|
+
dataType: "date_time"
|
|
513
|
+
},
|
|
514
|
+
image: {
|
|
515
|
+
displayName: "Thumbnail",
|
|
516
|
+
dataType: "image"
|
|
517
|
+
},
|
|
518
|
+
sg_uploaded_movie: {
|
|
519
|
+
displayName: "Uploaded Movie",
|
|
520
|
+
dataType: "url"
|
|
521
|
+
},
|
|
522
|
+
sg_bar_color: {
|
|
523
|
+
displayName: "Bar Colour",
|
|
524
|
+
dataType: "color"
|
|
525
|
+
},
|
|
526
|
+
project: {
|
|
527
|
+
displayName: "Project",
|
|
528
|
+
dataType: "entity",
|
|
529
|
+
mandatory: true,
|
|
530
|
+
validTypes: ["Project"]
|
|
531
|
+
},
|
|
532
|
+
entity: {
|
|
533
|
+
displayName: "Link",
|
|
534
|
+
dataType: "entity",
|
|
535
|
+
validTypes: [
|
|
536
|
+
"Shot",
|
|
537
|
+
"Asset",
|
|
538
|
+
"Sequence"
|
|
539
|
+
]
|
|
540
|
+
},
|
|
541
|
+
sg_task: {
|
|
542
|
+
displayName: "Task",
|
|
543
|
+
dataType: "entity",
|
|
544
|
+
validTypes: ["Task"]
|
|
545
|
+
},
|
|
546
|
+
user: {
|
|
547
|
+
displayName: "Artist",
|
|
548
|
+
dataType: "entity",
|
|
549
|
+
validTypes: ["HumanUser", "ApiUser"]
|
|
550
|
+
},
|
|
551
|
+
playlists: {
|
|
552
|
+
displayName: "Playlists",
|
|
553
|
+
dataType: "multi_entity",
|
|
554
|
+
validTypes: ["Playlist"]
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
Task: {
|
|
558
|
+
...AUDIT,
|
|
559
|
+
content: {
|
|
560
|
+
displayName: "Task Name",
|
|
561
|
+
dataType: "text",
|
|
562
|
+
mandatory: true
|
|
563
|
+
},
|
|
564
|
+
sg_description: {
|
|
565
|
+
displayName: "Description",
|
|
566
|
+
dataType: "text"
|
|
567
|
+
},
|
|
568
|
+
sg_status_list: statusSpec(TASK_STATUSES, "wtg"),
|
|
569
|
+
start_date: {
|
|
570
|
+
displayName: "Start Date",
|
|
571
|
+
dataType: "date"
|
|
572
|
+
},
|
|
573
|
+
due_date: {
|
|
574
|
+
displayName: "Due Date",
|
|
575
|
+
dataType: "date"
|
|
576
|
+
},
|
|
577
|
+
duration: {
|
|
578
|
+
displayName: "Duration",
|
|
579
|
+
dataType: "duration"
|
|
580
|
+
},
|
|
581
|
+
est_in_mins: {
|
|
582
|
+
displayName: "Bid",
|
|
583
|
+
dataType: "duration"
|
|
584
|
+
},
|
|
585
|
+
time_logs_sum: {
|
|
586
|
+
displayName: "Time Logged",
|
|
587
|
+
dataType: "duration",
|
|
588
|
+
editable: false
|
|
589
|
+
},
|
|
590
|
+
time_percent_of_est: {
|
|
591
|
+
displayName: "Time Percent of Bid",
|
|
592
|
+
dataType: "percent",
|
|
593
|
+
editable: false
|
|
594
|
+
},
|
|
595
|
+
color: {
|
|
596
|
+
displayName: "Task Color",
|
|
597
|
+
dataType: "color"
|
|
598
|
+
},
|
|
599
|
+
milestone: {
|
|
600
|
+
displayName: "Milestone",
|
|
601
|
+
dataType: "checkbox"
|
|
602
|
+
},
|
|
603
|
+
project: {
|
|
604
|
+
displayName: "Project",
|
|
605
|
+
dataType: "entity",
|
|
606
|
+
mandatory: true,
|
|
607
|
+
validTypes: ["Project"]
|
|
608
|
+
},
|
|
609
|
+
entity: {
|
|
610
|
+
displayName: "Link",
|
|
611
|
+
dataType: "entity",
|
|
612
|
+
validTypes: [
|
|
613
|
+
"Shot",
|
|
614
|
+
"Asset",
|
|
615
|
+
"Sequence"
|
|
616
|
+
]
|
|
617
|
+
},
|
|
618
|
+
step: {
|
|
619
|
+
displayName: "Pipeline Step",
|
|
620
|
+
dataType: "entity",
|
|
621
|
+
validTypes: ["Step"]
|
|
622
|
+
},
|
|
623
|
+
task_assignees: {
|
|
624
|
+
displayName: "Assigned To",
|
|
625
|
+
dataType: "multi_entity",
|
|
626
|
+
validTypes: ["Group", "HumanUser"]
|
|
627
|
+
},
|
|
628
|
+
task_reviewers: {
|
|
629
|
+
displayName: "Reviewers",
|
|
630
|
+
dataType: "multi_entity",
|
|
631
|
+
validTypes: ["Group", "HumanUser"]
|
|
632
|
+
},
|
|
633
|
+
upstream_tasks: {
|
|
634
|
+
displayName: "Upstream Tasks",
|
|
635
|
+
dataType: "multi_entity",
|
|
636
|
+
validTypes: ["Task"]
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
Note: {
|
|
640
|
+
...AUDIT,
|
|
641
|
+
subject: {
|
|
642
|
+
displayName: "Subject",
|
|
643
|
+
dataType: "text",
|
|
644
|
+
mandatory: true
|
|
645
|
+
},
|
|
646
|
+
content: {
|
|
647
|
+
displayName: "Body",
|
|
648
|
+
dataType: "text"
|
|
649
|
+
},
|
|
650
|
+
sg_status_list: statusSpec(NOTE_STATUSES, "opn", true),
|
|
651
|
+
sg_note_type: {
|
|
652
|
+
displayName: "Note Type",
|
|
653
|
+
dataType: "list",
|
|
654
|
+
validValues: NOTE_TYPES
|
|
655
|
+
},
|
|
656
|
+
read_by_current_user: {
|
|
657
|
+
displayName: "Read by Current User",
|
|
658
|
+
dataType: "list",
|
|
659
|
+
validValues: ["unread", "read"]
|
|
660
|
+
},
|
|
661
|
+
publish_status: {
|
|
662
|
+
displayName: "Publish Status",
|
|
663
|
+
dataType: "text"
|
|
664
|
+
},
|
|
665
|
+
project: {
|
|
666
|
+
displayName: "Project",
|
|
667
|
+
dataType: "entity",
|
|
668
|
+
validTypes: ["Project"]
|
|
669
|
+
},
|
|
670
|
+
user: {
|
|
671
|
+
displayName: "Author",
|
|
672
|
+
dataType: "entity",
|
|
673
|
+
validTypes: ["HumanUser", "ApiUser"]
|
|
674
|
+
},
|
|
675
|
+
note_links: {
|
|
676
|
+
displayName: "Link",
|
|
677
|
+
dataType: "multi_entity",
|
|
678
|
+
validTypes: [
|
|
679
|
+
"Shot",
|
|
680
|
+
"Asset",
|
|
681
|
+
"Sequence",
|
|
682
|
+
"Version",
|
|
683
|
+
"Playlist"
|
|
684
|
+
]
|
|
685
|
+
},
|
|
686
|
+
tasks: {
|
|
687
|
+
displayName: "Tasks",
|
|
688
|
+
dataType: "multi_entity",
|
|
689
|
+
validTypes: ["Task"]
|
|
690
|
+
},
|
|
691
|
+
replies: {
|
|
692
|
+
displayName: "Replies",
|
|
693
|
+
dataType: "multi_entity",
|
|
694
|
+
validTypes: ["Reply"]
|
|
695
|
+
},
|
|
696
|
+
attachments: {
|
|
697
|
+
displayName: "Attachments",
|
|
698
|
+
dataType: "multi_entity",
|
|
699
|
+
validTypes: ["Attachment"]
|
|
700
|
+
},
|
|
701
|
+
addressings_to: {
|
|
702
|
+
displayName: "To",
|
|
703
|
+
dataType: "multi_entity",
|
|
704
|
+
validTypes: ["Group", "HumanUser"]
|
|
705
|
+
},
|
|
706
|
+
addressings_cc: {
|
|
707
|
+
displayName: "Cc",
|
|
708
|
+
dataType: "multi_entity",
|
|
709
|
+
validTypes: ["Group", "HumanUser"]
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
Reply: {
|
|
713
|
+
id: AUDIT["id"],
|
|
714
|
+
cached_display_name: AUDIT["cached_display_name"],
|
|
715
|
+
created_at: AUDIT["created_at"],
|
|
716
|
+
content: {
|
|
717
|
+
displayName: "Reply Text",
|
|
718
|
+
dataType: "text",
|
|
719
|
+
mandatory: true
|
|
720
|
+
},
|
|
721
|
+
entity: {
|
|
722
|
+
displayName: "Link",
|
|
723
|
+
dataType: "entity",
|
|
724
|
+
validTypes: [
|
|
725
|
+
"Note",
|
|
726
|
+
"Version",
|
|
727
|
+
"Shot",
|
|
728
|
+
"Asset",
|
|
729
|
+
"Task"
|
|
730
|
+
]
|
|
731
|
+
},
|
|
732
|
+
user: {
|
|
733
|
+
displayName: "Author",
|
|
734
|
+
dataType: "entity",
|
|
735
|
+
validTypes: [
|
|
736
|
+
"HumanUser",
|
|
737
|
+
"ApiUser",
|
|
738
|
+
"ClientUser"
|
|
739
|
+
]
|
|
740
|
+
},
|
|
741
|
+
publish_status: {
|
|
742
|
+
displayName: "Publish Status",
|
|
743
|
+
dataType: "text"
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
Attachment: {
|
|
747
|
+
...AUDIT,
|
|
748
|
+
display_name: {
|
|
749
|
+
displayName: "File Display Name",
|
|
750
|
+
dataType: "text"
|
|
751
|
+
},
|
|
752
|
+
description: {
|
|
753
|
+
displayName: "Description",
|
|
754
|
+
dataType: "text"
|
|
755
|
+
},
|
|
756
|
+
original_fname: {
|
|
757
|
+
displayName: "Original Filename",
|
|
758
|
+
dataType: "text"
|
|
759
|
+
},
|
|
760
|
+
filename: {
|
|
761
|
+
displayName: "File Name",
|
|
762
|
+
dataType: "text",
|
|
763
|
+
editable: false
|
|
764
|
+
},
|
|
765
|
+
file_extension: {
|
|
766
|
+
displayName: "File Type",
|
|
767
|
+
dataType: "text",
|
|
768
|
+
editable: false
|
|
769
|
+
},
|
|
770
|
+
file_size: {
|
|
771
|
+
displayName: "File Size",
|
|
772
|
+
dataType: "number",
|
|
773
|
+
editable: false
|
|
774
|
+
},
|
|
775
|
+
this_file: {
|
|
776
|
+
displayName: "Link",
|
|
777
|
+
dataType: "url",
|
|
778
|
+
editable: false,
|
|
779
|
+
createOnly: true
|
|
780
|
+
},
|
|
781
|
+
processing_status: {
|
|
782
|
+
displayName: "Processing Status",
|
|
783
|
+
dataType: "list",
|
|
784
|
+
editable: false,
|
|
785
|
+
validValues: [
|
|
786
|
+
"thumbnail_pending",
|
|
787
|
+
"unverified",
|
|
788
|
+
"clean",
|
|
789
|
+
"infected"
|
|
790
|
+
]
|
|
791
|
+
},
|
|
792
|
+
sg_status_list: statusSpec(ATTACHMENT_STATUSES, "na"),
|
|
793
|
+
project: {
|
|
794
|
+
displayName: "Project",
|
|
795
|
+
dataType: "entity",
|
|
796
|
+
validTypes: ["Project"]
|
|
797
|
+
},
|
|
798
|
+
attachment_links: {
|
|
799
|
+
displayName: "Attachment Links",
|
|
800
|
+
dataType: "multi_entity",
|
|
801
|
+
validTypes: [
|
|
802
|
+
"Note",
|
|
803
|
+
"Version",
|
|
804
|
+
"Shot",
|
|
805
|
+
"Asset",
|
|
806
|
+
"Sequence",
|
|
807
|
+
"Delivery"
|
|
808
|
+
]
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
EventLogEntry: {
|
|
812
|
+
id: AUDIT["id"],
|
|
813
|
+
cached_display_name: AUDIT["cached_display_name"],
|
|
814
|
+
created_at: AUDIT["created_at"],
|
|
815
|
+
event_type: {
|
|
816
|
+
displayName: "Event Type",
|
|
817
|
+
dataType: "text",
|
|
818
|
+
editable: false
|
|
819
|
+
},
|
|
820
|
+
attribute_name: {
|
|
821
|
+
displayName: "Attribute Name",
|
|
822
|
+
dataType: "text",
|
|
823
|
+
editable: false
|
|
824
|
+
},
|
|
825
|
+
description: {
|
|
826
|
+
displayName: "Description",
|
|
827
|
+
dataType: "text",
|
|
828
|
+
editable: false
|
|
829
|
+
},
|
|
830
|
+
meta: {
|
|
831
|
+
displayName: "Meta",
|
|
832
|
+
dataType: "serializable",
|
|
833
|
+
editable: false
|
|
834
|
+
},
|
|
835
|
+
session_uuid: {
|
|
836
|
+
displayName: "Session UUID",
|
|
837
|
+
dataType: "uuid",
|
|
838
|
+
editable: false
|
|
839
|
+
},
|
|
840
|
+
entity: {
|
|
841
|
+
displayName: "Entity",
|
|
842
|
+
dataType: "entity",
|
|
843
|
+
editable: false,
|
|
844
|
+
validTypes: [
|
|
845
|
+
"Shot",
|
|
846
|
+
"Asset",
|
|
847
|
+
"Sequence",
|
|
848
|
+
"Version",
|
|
849
|
+
"Task",
|
|
850
|
+
"Note",
|
|
851
|
+
"Reply",
|
|
852
|
+
"Project"
|
|
853
|
+
]
|
|
854
|
+
},
|
|
855
|
+
project: {
|
|
856
|
+
displayName: "Project",
|
|
857
|
+
dataType: "entity",
|
|
858
|
+
editable: false,
|
|
859
|
+
validTypes: ["Project"]
|
|
860
|
+
},
|
|
861
|
+
user: {
|
|
862
|
+
displayName: "User",
|
|
863
|
+
dataType: "entity",
|
|
864
|
+
editable: false,
|
|
865
|
+
validTypes: ["HumanUser", "ApiUser"]
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
HumanUser: {
|
|
869
|
+
...AUDIT,
|
|
870
|
+
login: {
|
|
871
|
+
displayName: "Login",
|
|
872
|
+
dataType: "text",
|
|
873
|
+
unique: true
|
|
874
|
+
},
|
|
875
|
+
name: {
|
|
876
|
+
displayName: "Name",
|
|
877
|
+
dataType: "text",
|
|
878
|
+
mandatory: true
|
|
879
|
+
},
|
|
880
|
+
firstname: {
|
|
881
|
+
displayName: "First Name",
|
|
882
|
+
dataType: "text"
|
|
883
|
+
},
|
|
884
|
+
lastname: {
|
|
885
|
+
displayName: "Last Name",
|
|
886
|
+
dataType: "text"
|
|
887
|
+
},
|
|
888
|
+
email: {
|
|
889
|
+
displayName: "Email",
|
|
890
|
+
dataType: "text"
|
|
891
|
+
},
|
|
892
|
+
sg_status_list: statusSpec(USER_STATUSES, "act"),
|
|
893
|
+
image: {
|
|
894
|
+
displayName: "Thumbnail",
|
|
895
|
+
dataType: "image"
|
|
896
|
+
},
|
|
897
|
+
password_proxy: {
|
|
898
|
+
displayName: "Password",
|
|
899
|
+
dataType: "password",
|
|
900
|
+
editable: false
|
|
901
|
+
},
|
|
902
|
+
can_impersonate_this_user: {
|
|
903
|
+
displayName: "Can Impersonate",
|
|
904
|
+
dataType: "checkbox",
|
|
905
|
+
editable: false
|
|
906
|
+
},
|
|
907
|
+
sg_department_name: {
|
|
908
|
+
displayName: "Department Name",
|
|
909
|
+
dataType: "text"
|
|
910
|
+
},
|
|
911
|
+
projects: {
|
|
912
|
+
displayName: "Projects",
|
|
913
|
+
dataType: "multi_entity",
|
|
914
|
+
validTypes: ["Project"]
|
|
915
|
+
},
|
|
916
|
+
groups: {
|
|
917
|
+
displayName: "Groups",
|
|
918
|
+
dataType: "multi_entity",
|
|
919
|
+
validTypes: ["Group"]
|
|
920
|
+
},
|
|
921
|
+
permission_rule_set: {
|
|
922
|
+
displayName: "Permission Rule Set",
|
|
923
|
+
dataType: "entity",
|
|
924
|
+
editable: false,
|
|
925
|
+
validTypes: ["PermissionRuleSet"]
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
ApiUser: {
|
|
929
|
+
...AUDIT,
|
|
930
|
+
firstname: {
|
|
931
|
+
displayName: "Script Name",
|
|
932
|
+
dataType: "text"
|
|
933
|
+
},
|
|
934
|
+
email: {
|
|
935
|
+
displayName: "Email",
|
|
936
|
+
dataType: "text"
|
|
937
|
+
},
|
|
938
|
+
description: {
|
|
939
|
+
displayName: "Description",
|
|
940
|
+
dataType: "text"
|
|
941
|
+
},
|
|
942
|
+
generate_event_log_entries: {
|
|
943
|
+
displayName: "Generate Events",
|
|
944
|
+
dataType: "checkbox"
|
|
945
|
+
},
|
|
946
|
+
projects: {
|
|
947
|
+
displayName: "Projects",
|
|
948
|
+
dataType: "multi_entity",
|
|
949
|
+
validTypes: ["Project"]
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
Step: {
|
|
953
|
+
...AUDIT,
|
|
954
|
+
code: {
|
|
955
|
+
displayName: "Step Name",
|
|
956
|
+
dataType: "text",
|
|
957
|
+
mandatory: true
|
|
958
|
+
},
|
|
959
|
+
short_name: {
|
|
960
|
+
displayName: "Short Name",
|
|
961
|
+
dataType: "text",
|
|
962
|
+
mandatory: true
|
|
963
|
+
},
|
|
964
|
+
entity_type: {
|
|
965
|
+
displayName: "Entity Type",
|
|
966
|
+
dataType: "entity_type",
|
|
967
|
+
editable: false
|
|
968
|
+
},
|
|
969
|
+
list_order: {
|
|
970
|
+
displayName: "Sort Order",
|
|
971
|
+
dataType: "number"
|
|
972
|
+
},
|
|
973
|
+
color: {
|
|
974
|
+
displayName: "Color",
|
|
975
|
+
dataType: "color"
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
Status: {
|
|
979
|
+
...AUDIT,
|
|
980
|
+
code: {
|
|
981
|
+
displayName: "Status Code",
|
|
982
|
+
dataType: "text",
|
|
983
|
+
unique: true
|
|
984
|
+
},
|
|
985
|
+
name: {
|
|
986
|
+
displayName: "Status Name",
|
|
987
|
+
dataType: "text"
|
|
988
|
+
},
|
|
989
|
+
bg_color: {
|
|
990
|
+
displayName: "Background Colour",
|
|
991
|
+
dataType: "color"
|
|
992
|
+
},
|
|
993
|
+
icon: {
|
|
994
|
+
displayName: "Icon",
|
|
995
|
+
dataType: "entity",
|
|
996
|
+
validTypes: ["Icon"]
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
Icon: {
|
|
1000
|
+
...AUDIT,
|
|
1001
|
+
name: {
|
|
1002
|
+
displayName: "Icon Name",
|
|
1003
|
+
dataType: "text"
|
|
1004
|
+
},
|
|
1005
|
+
display_type: {
|
|
1006
|
+
displayName: "Display Type",
|
|
1007
|
+
dataType: "list",
|
|
1008
|
+
validValues: [
|
|
1009
|
+
"image_map",
|
|
1010
|
+
"image",
|
|
1011
|
+
"html"
|
|
1012
|
+
]
|
|
1013
|
+
},
|
|
1014
|
+
icon_type: {
|
|
1015
|
+
displayName: "Icon Type",
|
|
1016
|
+
dataType: "list",
|
|
1017
|
+
validValues: ["permanent_status", "custom_status"]
|
|
1018
|
+
},
|
|
1019
|
+
image_map_key: {
|
|
1020
|
+
displayName: "Image Map Key",
|
|
1021
|
+
dataType: "text"
|
|
1022
|
+
},
|
|
1023
|
+
url: {
|
|
1024
|
+
displayName: "URL",
|
|
1025
|
+
dataType: "text"
|
|
1026
|
+
},
|
|
1027
|
+
image_data: {
|
|
1028
|
+
displayName: "Image Data",
|
|
1029
|
+
dataType: "text"
|
|
1030
|
+
},
|
|
1031
|
+
html: {
|
|
1032
|
+
displayName: "HTML",
|
|
1033
|
+
dataType: "text"
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
/**
|
|
1038
|
+
* The identity field per type. It is flagged `mandatory` and is optional on a
|
|
1039
|
+
* create; the server fills it with `New <display name> <id>` on the types below,
|
|
1040
|
+
* and a Note is left titleless (012_create_version, entity_types/Note).
|
|
1041
|
+
*/
|
|
1042
|
+
const IDENTITY_FIELD = {
|
|
1043
|
+
Project: "name",
|
|
1044
|
+
Sequence: "code",
|
|
1045
|
+
Shot: "code",
|
|
1046
|
+
Asset: "code",
|
|
1047
|
+
Version: "code",
|
|
1048
|
+
Task: "content",
|
|
1049
|
+
Note: "subject",
|
|
1050
|
+
Reply: "content",
|
|
1051
|
+
Attachment: "display_name"
|
|
1052
|
+
};
|
|
1053
|
+
const GENERATED_IDENTITY = /* @__PURE__ */ new Set([
|
|
1054
|
+
"Sequence",
|
|
1055
|
+
"Shot",
|
|
1056
|
+
"Asset",
|
|
1057
|
+
"Version",
|
|
1058
|
+
"Task"
|
|
1059
|
+
]);
|
|
1060
|
+
const DISPLAY_NAMES = {
|
|
1061
|
+
Project: "Project",
|
|
1062
|
+
Sequence: "Sequence",
|
|
1063
|
+
Shot: "Shot",
|
|
1064
|
+
Asset: "Asset",
|
|
1065
|
+
Version: "Version",
|
|
1066
|
+
Task: "Task",
|
|
1067
|
+
HumanUser: "Person",
|
|
1068
|
+
ApiUser: "Script",
|
|
1069
|
+
Step: "Pipeline Step",
|
|
1070
|
+
Status: "Status",
|
|
1071
|
+
Icon: "Icon",
|
|
1072
|
+
Note: "Note",
|
|
1073
|
+
Reply: "Reply",
|
|
1074
|
+
Attachment: "Attachment",
|
|
1075
|
+
EventLogEntry: "Event Log Entry"
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* Which codes each project hides, per type. `hidden_values` is the only thing
|
|
1079
|
+
* `project_id` changes (009_status_lists), and it is not a subset of
|
|
1080
|
+
* `valid_values`: the probed site hid `blk` and `rdy`, neither of them valid.
|
|
1081
|
+
*/
|
|
1082
|
+
const HIDDEN_VALUES = {
|
|
1083
|
+
70: {
|
|
1084
|
+
"Version.sg_status_list": [
|
|
1085
|
+
"part",
|
|
1086
|
+
"pass",
|
|
1087
|
+
"pndad",
|
|
1088
|
+
"pndl",
|
|
1089
|
+
"pndvs",
|
|
1090
|
+
"pndng"
|
|
1091
|
+
],
|
|
1092
|
+
"Task.sg_status_list": ["omt", "dis"],
|
|
1093
|
+
"Shot.sg_status_list": ["omt"],
|
|
1094
|
+
"Asset.sg_status_list": ["omt"],
|
|
1095
|
+
"Sequence.sg_status_list": [],
|
|
1096
|
+
"Project.sg_status": []
|
|
1097
|
+
},
|
|
1098
|
+
71: {
|
|
1099
|
+
"Version.sg_status_list": ["pndl", "pndvs"],
|
|
1100
|
+
"Task.sg_status_list": [
|
|
1101
|
+
"dis",
|
|
1102
|
+
"hld",
|
|
1103
|
+
"blk"
|
|
1104
|
+
],
|
|
1105
|
+
"Shot.sg_status_list": ["hld", "omt"],
|
|
1106
|
+
"Asset.sg_status_list": [],
|
|
1107
|
+
"Sequence.sg_status_list": [],
|
|
1108
|
+
"Project.sg_status": ["On Hold"]
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
/** mulberry32: a small, fast, seedable PRNG so a seed reproduces a whole site. */
|
|
1112
|
+
function mulberry32(seed) {
|
|
1113
|
+
let a = seed >>> 0;
|
|
1114
|
+
return () => {
|
|
1115
|
+
a = a + 1831565813 >>> 0;
|
|
1116
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
1117
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1118
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
/** The mock site's today. Every fixture date is an offset in days from it. */
|
|
1122
|
+
const EPOCH = Date.UTC(2026, 0, 5);
|
|
1123
|
+
/** Midday on the mock site's today, for a caller pinning `now` so relative-date filters land on the fixtures. */
|
|
1124
|
+
const MOCK_NOW = `${new Date(EPOCH + 432e5).toISOString().slice(0, 19)}Z`;
|
|
1125
|
+
function isoDate(dayOffset) {
|
|
1126
|
+
return new Date(EPOCH + dayOffset * 864e5).toISOString().slice(0, 10);
|
|
1127
|
+
}
|
|
1128
|
+
/** `YYYY-MM-DDTHH:MM:SSZ`: second resolution, literal Z, never an offset (field_types/date_time). */
|
|
1129
|
+
function isoDateTime(dayOffset, seconds = 0) {
|
|
1130
|
+
return `${new Date(EPOCH + dayOffset * 864e5 + seconds * 1e3).toISOString().slice(0, 19)}Z`;
|
|
1131
|
+
}
|
|
1132
|
+
function thumb(slug, w = 96, h = 54) {
|
|
1133
|
+
return `https://picsum.photos/seed/${slug}/${w}/${h}`;
|
|
1134
|
+
}
|
|
1135
|
+
/** The web root of the mock site, which is where a transcoding placeholder lives. */
|
|
1136
|
+
const MOCK_SITE_URL = "https://mock.example.studio";
|
|
1137
|
+
/** Illustrated portraits from DiceBear's CC0 "lorelei" set, one per login, so no real face appears. */
|
|
1138
|
+
function portrait(login) {
|
|
1139
|
+
return `https://api.dicebear.com/9.x/lorelei/svg?seed=${encodeURIComponent(login)}&backgroundType=gradientLinear&backgroundColor=d1d4f9,c0aede,ffdfbf,b6e3f4`;
|
|
1140
|
+
}
|
|
1141
|
+
function ref(row) {
|
|
1142
|
+
return {
|
|
1143
|
+
type: row.type,
|
|
1144
|
+
id: row.id
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const STEP_SEEDS = [
|
|
1148
|
+
{
|
|
1149
|
+
code: "Layout",
|
|
1150
|
+
short_name: "layout",
|
|
1151
|
+
entity_type: "Shot",
|
|
1152
|
+
color: "110,180,200"
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
code: "Animation",
|
|
1156
|
+
short_name: "ANM",
|
|
1157
|
+
entity_type: "Shot",
|
|
1158
|
+
color: "253,94,99"
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
code: "Character FX",
|
|
1162
|
+
short_name: "CFX",
|
|
1163
|
+
entity_type: "Shot",
|
|
1164
|
+
color: "190,120,220"
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
code: "Lighting",
|
|
1168
|
+
short_name: "LGT",
|
|
1169
|
+
entity_type: "Shot",
|
|
1170
|
+
color: "240,190,90"
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
code: "FX",
|
|
1174
|
+
short_name: "FX",
|
|
1175
|
+
entity_type: "Shot",
|
|
1176
|
+
color: "90,200,160"
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
code: "Comp",
|
|
1180
|
+
short_name: "CMP",
|
|
1181
|
+
entity_type: "Shot",
|
|
1182
|
+
color: "70,140,230"
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
code: "Model",
|
|
1186
|
+
short_name: "MDL",
|
|
1187
|
+
entity_type: "Asset",
|
|
1188
|
+
color: "200,140,90"
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
code: "Rig",
|
|
1192
|
+
short_name: "RIG",
|
|
1193
|
+
entity_type: "Asset",
|
|
1194
|
+
color: "150,150,220"
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
code: "Texture",
|
|
1198
|
+
short_name: "TXT",
|
|
1199
|
+
entity_type: "Asset",
|
|
1200
|
+
color: "220,150,110"
|
|
1201
|
+
}
|
|
1202
|
+
];
|
|
1203
|
+
const USER_SEEDS = [
|
|
1204
|
+
{
|
|
1205
|
+
first: "Ada",
|
|
1206
|
+
last: "Lovelace",
|
|
1207
|
+
dept: "Animation",
|
|
1208
|
+
status: "act"
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
first: "Bo",
|
|
1212
|
+
last: "Chen",
|
|
1213
|
+
dept: "Lighting",
|
|
1214
|
+
status: "dis"
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
first: "Cleo",
|
|
1218
|
+
last: "Dias",
|
|
1219
|
+
dept: "Comp",
|
|
1220
|
+
status: "act"
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
first: "Dmitri",
|
|
1224
|
+
last: "Ivanov",
|
|
1225
|
+
dept: "FX",
|
|
1226
|
+
status: "act"
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
first: "Eve",
|
|
1230
|
+
last: "Kim",
|
|
1231
|
+
dept: "Layout",
|
|
1232
|
+
status: "dis"
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
first: "Farid",
|
|
1236
|
+
last: "Nasser",
|
|
1237
|
+
dept: "Modelling",
|
|
1238
|
+
status: "act"
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
first: "Grace",
|
|
1242
|
+
last: "Ono",
|
|
1243
|
+
dept: "Rigging",
|
|
1244
|
+
status: "act"
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
first: "Hiro",
|
|
1248
|
+
last: "Tanaka",
|
|
1249
|
+
dept: "Production",
|
|
1250
|
+
status: "act"
|
|
1251
|
+
}
|
|
1252
|
+
];
|
|
1253
|
+
/**
|
|
1254
|
+
* The threads. `day` and the minute offsets place every row of a thread on one
|
|
1255
|
+
* timeline, so the Note, its Attachments and its Replies interleave in time order
|
|
1256
|
+
* the way `thread_contents` returns them.
|
|
1257
|
+
*/
|
|
1258
|
+
const NOTE_SEEDS = [
|
|
1259
|
+
{
|
|
1260
|
+
subject: "Key light reads flat",
|
|
1261
|
+
content: "The key reads flat against the plate. Warmer, and a stop down.",
|
|
1262
|
+
day: -6,
|
|
1263
|
+
author: 0,
|
|
1264
|
+
status: "opn",
|
|
1265
|
+
read: "unread",
|
|
1266
|
+
noteType: "Internal",
|
|
1267
|
+
attachments: [{
|
|
1268
|
+
filename: "key_light_ref.png",
|
|
1269
|
+
minutes: 2
|
|
1270
|
+
}, {
|
|
1271
|
+
filename: "plate_compare.png",
|
|
1272
|
+
minutes: 60
|
|
1273
|
+
}],
|
|
1274
|
+
replies: [{
|
|
1275
|
+
author: 2,
|
|
1276
|
+
minutes: 45,
|
|
1277
|
+
content: "Warmed it by 300K and dropped the key."
|
|
1278
|
+
}, {
|
|
1279
|
+
author: 0,
|
|
1280
|
+
minutes: 90,
|
|
1281
|
+
content: "Better. Leave the rim where it is."
|
|
1282
|
+
}]
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
subject: "Comp edges on the rotoscope",
|
|
1286
|
+
content: "The left edge tears on frame 1042.",
|
|
1287
|
+
day: -4,
|
|
1288
|
+
author: 2,
|
|
1289
|
+
status: "opn",
|
|
1290
|
+
read: "unread",
|
|
1291
|
+
noteType: "Client",
|
|
1292
|
+
attachments: [{
|
|
1293
|
+
filename: "frame_1042.png",
|
|
1294
|
+
minutes: 5
|
|
1295
|
+
}],
|
|
1296
|
+
replies: [{
|
|
1297
|
+
author: 5,
|
|
1298
|
+
minutes: 200,
|
|
1299
|
+
content: "Repainted the edge and pushed a new version."
|
|
1300
|
+
}]
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
subject: "Approved for the reel",
|
|
1304
|
+
content: "Nothing else from me.",
|
|
1305
|
+
day: -3,
|
|
1306
|
+
author: 7,
|
|
1307
|
+
status: "clsd",
|
|
1308
|
+
read: "read",
|
|
1309
|
+
noteType: "Internal",
|
|
1310
|
+
attachments: [],
|
|
1311
|
+
replies: []
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
subject: "Dust pass is too heavy",
|
|
1315
|
+
content: "Half the density and keep the drift.",
|
|
1316
|
+
day: -2,
|
|
1317
|
+
author: 3,
|
|
1318
|
+
status: "opn",
|
|
1319
|
+
read: "read",
|
|
1320
|
+
noteType: "Direction",
|
|
1321
|
+
attachments: [],
|
|
1322
|
+
replies: [{
|
|
1323
|
+
author: 5,
|
|
1324
|
+
minutes: 30,
|
|
1325
|
+
content: "Halved it."
|
|
1326
|
+
}, {
|
|
1327
|
+
author: 3,
|
|
1328
|
+
minutes: 120,
|
|
1329
|
+
content: "That reads."
|
|
1330
|
+
}]
|
|
1331
|
+
}
|
|
1332
|
+
];
|
|
1333
|
+
const ASSET_SEEDS = [
|
|
1334
|
+
{
|
|
1335
|
+
code: "charAda",
|
|
1336
|
+
type: "Character",
|
|
1337
|
+
project: 0
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
code: "charBruno",
|
|
1341
|
+
type: "Character",
|
|
1342
|
+
project: 0
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
code: "propLantern",
|
|
1346
|
+
type: "Prop",
|
|
1347
|
+
project: 0
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
code: "propCrate",
|
|
1351
|
+
type: "Prop",
|
|
1352
|
+
project: 0
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
code: "envForest",
|
|
1356
|
+
type: "Environment",
|
|
1357
|
+
project: 0
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
code: "envStation",
|
|
1361
|
+
type: "Environment",
|
|
1362
|
+
project: 0
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
code: "vehRover",
|
|
1366
|
+
type: "Vehicle",
|
|
1367
|
+
project: 0
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
code: "charLuna",
|
|
1371
|
+
type: "Character",
|
|
1372
|
+
project: 1
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
code: "envHarbour",
|
|
1376
|
+
type: "Environment",
|
|
1377
|
+
project: 1
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
code: "fxDust",
|
|
1381
|
+
type: "FX",
|
|
1382
|
+
project: 1
|
|
1383
|
+
}
|
|
1384
|
+
];
|
|
1385
|
+
function pick(rng, items) {
|
|
1386
|
+
return items[Math.floor(rng() * items.length)] ?? items[0];
|
|
1387
|
+
}
|
|
1388
|
+
function buildFixtures(seed, counts = {}) {
|
|
1389
|
+
const rng = mulberry32(seed);
|
|
1390
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1391
|
+
const index = /* @__PURE__ */ new Map();
|
|
1392
|
+
const add = (type, id, values) => {
|
|
1393
|
+
const row = {
|
|
1394
|
+
type,
|
|
1395
|
+
id,
|
|
1396
|
+
values: {
|
|
1397
|
+
...values,
|
|
1398
|
+
id
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
const list = rows.get(type);
|
|
1402
|
+
if (list) list.push(row);
|
|
1403
|
+
else rows.set(type, [row]);
|
|
1404
|
+
index.set(`${type}:${id}`, row);
|
|
1405
|
+
return row;
|
|
1406
|
+
};
|
|
1407
|
+
const users = USER_SEEDS.map((u, i) => {
|
|
1408
|
+
const login = `${u.first.toLowerCase()}.${u.last.toLowerCase()}`;
|
|
1409
|
+
const name = `${u.first} ${u.last}`;
|
|
1410
|
+
return add("HumanUser", 20 + i, {
|
|
1411
|
+
login,
|
|
1412
|
+
name,
|
|
1413
|
+
cached_display_name: name,
|
|
1414
|
+
firstname: u.first,
|
|
1415
|
+
lastname: u.last,
|
|
1416
|
+
email: `${login}@example.studio`,
|
|
1417
|
+
sg_status_list: u.status,
|
|
1418
|
+
image: portrait(login),
|
|
1419
|
+
password_proxy: "*******",
|
|
1420
|
+
can_impersonate_this_user: true,
|
|
1421
|
+
sg_department_name: u.dept,
|
|
1422
|
+
projects: [],
|
|
1423
|
+
groups: [],
|
|
1424
|
+
permission_rule_set: {
|
|
1425
|
+
type: "PermissionRuleSet",
|
|
1426
|
+
id: 8
|
|
1427
|
+
},
|
|
1428
|
+
created_at: isoDateTime(-200 + i),
|
|
1429
|
+
updated_at: isoDateTime(-10 + i),
|
|
1430
|
+
created_by: null,
|
|
1431
|
+
updated_by: null
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1434
|
+
const activeUsers = users.filter((u) => u.values["sg_status_list"] === "act");
|
|
1435
|
+
const apiUsers = ["sg_widgets_demo", "pipeline_bot"].map((n, i) => add("ApiUser", 90 + i, {
|
|
1436
|
+
firstname: n,
|
|
1437
|
+
cached_display_name: n,
|
|
1438
|
+
email: `${n}@example.studio`,
|
|
1439
|
+
description: i === 0 ? "Widget demos" : "Nightly publishes",
|
|
1440
|
+
generate_event_log_entries: i === 0,
|
|
1441
|
+
projects: [],
|
|
1442
|
+
created_at: isoDateTime(-300 + i),
|
|
1443
|
+
updated_at: isoDateTime(-300 + i),
|
|
1444
|
+
created_by: null,
|
|
1445
|
+
updated_by: null
|
|
1446
|
+
}));
|
|
1447
|
+
const bot = apiUsers[0];
|
|
1448
|
+
const steps = STEP_SEEDS.map((s, i) => add("Step", 1 + i, {
|
|
1449
|
+
code: s.code,
|
|
1450
|
+
short_name: s.short_name,
|
|
1451
|
+
cached_display_name: null,
|
|
1452
|
+
entity_type: s.entity_type,
|
|
1453
|
+
list_order: i + 1,
|
|
1454
|
+
color: s.color,
|
|
1455
|
+
created_at: isoDateTime(-400),
|
|
1456
|
+
updated_at: isoDateTime(-400),
|
|
1457
|
+
created_by: null,
|
|
1458
|
+
updated_by: null
|
|
1459
|
+
}));
|
|
1460
|
+
[.../* @__PURE__ */ new Set([
|
|
1461
|
+
...VERSION_STATUSES,
|
|
1462
|
+
...TASK_STATUSES,
|
|
1463
|
+
...SHOT_STATUSES,
|
|
1464
|
+
...SEQUENCE_STATUSES,
|
|
1465
|
+
...USER_STATUSES,
|
|
1466
|
+
...NOTE_STATUSES,
|
|
1467
|
+
...ATTACHMENT_STATUSES
|
|
1468
|
+
])].forEach((code, i) => {
|
|
1469
|
+
const iconId = 400 + i;
|
|
1470
|
+
const iconValues = code === "custom" ? {
|
|
1471
|
+
name: "CustomIcon",
|
|
1472
|
+
display_type: "image",
|
|
1473
|
+
icon_type: "custom_status",
|
|
1474
|
+
image_map_key: null,
|
|
1475
|
+
url: TINY_PNG_DATA_URL,
|
|
1476
|
+
image_data: TINY_PNG_DATA_URL.split(",")[1] ?? "",
|
|
1477
|
+
html: null
|
|
1478
|
+
} : code === "act" ? {
|
|
1479
|
+
name: "ActiveBadge",
|
|
1480
|
+
display_type: "html",
|
|
1481
|
+
icon_type: "custom_status",
|
|
1482
|
+
image_map_key: null,
|
|
1483
|
+
url: "",
|
|
1484
|
+
image_data: null,
|
|
1485
|
+
html: "Active"
|
|
1486
|
+
} : {
|
|
1487
|
+
name: `icon_${code}`,
|
|
1488
|
+
display_type: "image_map",
|
|
1489
|
+
icon_type: "permanent_status",
|
|
1490
|
+
image_map_key: `icon_${code}`,
|
|
1491
|
+
url: "",
|
|
1492
|
+
image_data: null,
|
|
1493
|
+
html: null
|
|
1494
|
+
};
|
|
1495
|
+
const icon = add("Icon", iconId, {
|
|
1496
|
+
...iconValues,
|
|
1497
|
+
cached_display_name: iconValues["name"],
|
|
1498
|
+
created_at: isoDateTime(-500),
|
|
1499
|
+
updated_at: isoDateTime(-500),
|
|
1500
|
+
created_by: null,
|
|
1501
|
+
updated_by: null
|
|
1502
|
+
});
|
|
1503
|
+
const name = STATUS_DISPLAY[code] ?? code;
|
|
1504
|
+
add("Status", 300 + i, {
|
|
1505
|
+
code,
|
|
1506
|
+
name,
|
|
1507
|
+
cached_display_name: name,
|
|
1508
|
+
bg_color: STATUS_BG[code] ?? "150,150,150",
|
|
1509
|
+
icon: ref(icon),
|
|
1510
|
+
created_at: isoDateTime(-500),
|
|
1511
|
+
updated_at: isoDateTime(-500),
|
|
1512
|
+
created_by: null,
|
|
1513
|
+
updated_by: null
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
const projects = [
|
|
1517
|
+
{
|
|
1518
|
+
id: 70,
|
|
1519
|
+
name: "Blue Moon Rising",
|
|
1520
|
+
code: "bmr",
|
|
1521
|
+
status: "Active",
|
|
1522
|
+
type: "Feature"
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
id: 71,
|
|
1526
|
+
name: "Harbour Lights",
|
|
1527
|
+
code: "hbl",
|
|
1528
|
+
status: "Bidding",
|
|
1529
|
+
type: "Episodic"
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
id: 72,
|
|
1533
|
+
name: "Night Ferry",
|
|
1534
|
+
code: "nfr",
|
|
1535
|
+
status: "Active",
|
|
1536
|
+
type: "Short"
|
|
1537
|
+
}
|
|
1538
|
+
].map((p, i) => add("Project", p.id, {
|
|
1539
|
+
name: p.name,
|
|
1540
|
+
code: p.code,
|
|
1541
|
+
cached_display_name: p.name,
|
|
1542
|
+
tank_name: p.code,
|
|
1543
|
+
sg_status: p.status,
|
|
1544
|
+
sg_type: p.type,
|
|
1545
|
+
sg_description: `${p.name}, the ${p.type.toLowerCase()}.`,
|
|
1546
|
+
sg_start_date: isoDate(-120 + i * 30),
|
|
1547
|
+
sg_end_date: isoDate(240 + i * 30),
|
|
1548
|
+
sg_frame_rate: 24,
|
|
1549
|
+
sg_progress: 40 + i * 15,
|
|
1550
|
+
archived: false,
|
|
1551
|
+
is_demo: false,
|
|
1552
|
+
is_template: false,
|
|
1553
|
+
image: thumb(p.code, 128, 72),
|
|
1554
|
+
landing_page_url: `/detail/Project/${p.id}?legacy=true`,
|
|
1555
|
+
users: activeUsers.map(ref),
|
|
1556
|
+
created_at: isoDateTime(-150 + i),
|
|
1557
|
+
updated_at: isoDateTime(-5 + i),
|
|
1558
|
+
created_by: ref(bot),
|
|
1559
|
+
updated_by: ref(bot)
|
|
1560
|
+
}));
|
|
1561
|
+
const p0 = projects[0];
|
|
1562
|
+
const p1 = projects[1];
|
|
1563
|
+
const p2 = projects[2];
|
|
1564
|
+
for (const u of users) u.values["projects"] = [
|
|
1565
|
+
ref(p0),
|
|
1566
|
+
ref(p1),
|
|
1567
|
+
ref(p2)
|
|
1568
|
+
];
|
|
1569
|
+
for (const a of apiUsers) a.values["projects"] = [
|
|
1570
|
+
ref(p0),
|
|
1571
|
+
ref(p1),
|
|
1572
|
+
ref(p2)
|
|
1573
|
+
];
|
|
1574
|
+
const sequences = [
|
|
1575
|
+
{
|
|
1576
|
+
code: "sh010",
|
|
1577
|
+
project: p0
|
|
1578
|
+
},
|
|
1579
|
+
{
|
|
1580
|
+
code: "sh020",
|
|
1581
|
+
project: p0
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
code: "sh030",
|
|
1585
|
+
project: p0
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
code: "sh040",
|
|
1589
|
+
project: p0
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
code: "hb010",
|
|
1593
|
+
project: p1
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
code: "hb020",
|
|
1597
|
+
project: p1
|
|
1598
|
+
}
|
|
1599
|
+
].map((s, i) => add("Sequence", 100 + i, {
|
|
1600
|
+
code: s.code,
|
|
1601
|
+
cached_display_name: s.code,
|
|
1602
|
+
description: `Sequence ${s.code}`,
|
|
1603
|
+
sg_status_list: pick(rng, SEQUENCE_STATUSES),
|
|
1604
|
+
sg_timecode: 36e5 + i * 6e4,
|
|
1605
|
+
sg_cut_duration: 200 + Math.floor(rng() * 400),
|
|
1606
|
+
image: thumb(s.code),
|
|
1607
|
+
project: ref(s.project),
|
|
1608
|
+
shots: [],
|
|
1609
|
+
created_at: isoDateTime(-140 + i),
|
|
1610
|
+
updated_at: isoDateTime(-20 + i),
|
|
1611
|
+
created_by: ref(bot),
|
|
1612
|
+
updated_by: ref(bot)
|
|
1613
|
+
}));
|
|
1614
|
+
const shots = [];
|
|
1615
|
+
const shotsPerSequence = [
|
|
1616
|
+
7,
|
|
1617
|
+
6,
|
|
1618
|
+
5,
|
|
1619
|
+
4,
|
|
1620
|
+
5,
|
|
1621
|
+
3
|
|
1622
|
+
];
|
|
1623
|
+
let shotId = 862;
|
|
1624
|
+
sequences.forEach((seq, si) => {
|
|
1625
|
+
const count = shotsPerSequence[si] ?? 5;
|
|
1626
|
+
for (let n = 0; n < count; n += 1) {
|
|
1627
|
+
const code = `${String(seq.values["code"])}_${String((n + 1) * 10).padStart(4, "0")}`;
|
|
1628
|
+
const cutIn = 1001;
|
|
1629
|
+
const duration = 40 + Math.floor(rng() * 160);
|
|
1630
|
+
const shot = add("Shot", shotId, {
|
|
1631
|
+
code,
|
|
1632
|
+
cached_display_name: code,
|
|
1633
|
+
description: `Shot ${code}`,
|
|
1634
|
+
sg_status_list: pick(rng, SHOT_STATUSES),
|
|
1635
|
+
sg_shot_type: pick(rng, SHOT_TYPES),
|
|
1636
|
+
sg_cut_in: cutIn,
|
|
1637
|
+
sg_cut_out: cutIn + duration,
|
|
1638
|
+
sg_cut_duration: duration,
|
|
1639
|
+
sg_working_duration: duration * 5,
|
|
1640
|
+
sg_turnover_date: isoDate(Math.floor(rng() * 120)),
|
|
1641
|
+
sg_complexity: Math.floor(rng() * 101),
|
|
1642
|
+
sg_lens: 24 + Math.round(rng() * 800) / 10,
|
|
1643
|
+
sg_omit: false,
|
|
1644
|
+
image: shotId === 865 ? null : thumb(code),
|
|
1645
|
+
sg_shot_notes_url: null,
|
|
1646
|
+
project: seq.values["project"],
|
|
1647
|
+
sg_sequence: ref(seq),
|
|
1648
|
+
assets: [],
|
|
1649
|
+
tasks: [],
|
|
1650
|
+
created_at: isoDateTime(-130 + shotId % 40),
|
|
1651
|
+
updated_at: isoDateTime(-15 + shotId % 10),
|
|
1652
|
+
created_by: ref(bot),
|
|
1653
|
+
updated_by: ref(bot)
|
|
1654
|
+
});
|
|
1655
|
+
shots.push(shot);
|
|
1656
|
+
seq.values["shots"].push(ref(shot));
|
|
1657
|
+
shotId += 1;
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
[
|
|
1661
|
+
"nf_0010",
|
|
1662
|
+
"nf_0020",
|
|
1663
|
+
"nf_0030"
|
|
1664
|
+
].forEach((code, i) => {
|
|
1665
|
+
const duration = 60 + i * 20;
|
|
1666
|
+
add("Shot", shotId + i, {
|
|
1667
|
+
code,
|
|
1668
|
+
cached_display_name: code,
|
|
1669
|
+
description: `Shot ${code}`,
|
|
1670
|
+
sg_status_list: pick(rng, SHOT_STATUSES),
|
|
1671
|
+
sg_shot_type: pick(rng, SHOT_TYPES),
|
|
1672
|
+
sg_cut_in: 1001,
|
|
1673
|
+
sg_cut_out: 1001 + duration,
|
|
1674
|
+
sg_cut_duration: duration,
|
|
1675
|
+
sg_working_duration: duration * 5,
|
|
1676
|
+
sg_turnover_date: isoDate(Math.floor(rng() * 120)),
|
|
1677
|
+
sg_complexity: Math.floor(rng() * 101),
|
|
1678
|
+
sg_lens: 24 + Math.round(rng() * 800) / 10,
|
|
1679
|
+
sg_omit: false,
|
|
1680
|
+
image: thumb(code),
|
|
1681
|
+
sg_shot_notes_url: null,
|
|
1682
|
+
project: ref(p2),
|
|
1683
|
+
sg_sequence: null,
|
|
1684
|
+
assets: [],
|
|
1685
|
+
tasks: [],
|
|
1686
|
+
created_at: isoDateTime(-100 + i),
|
|
1687
|
+
updated_at: isoDateTime(-10 + i),
|
|
1688
|
+
created_by: ref(bot),
|
|
1689
|
+
updated_by: ref(bot)
|
|
1690
|
+
});
|
|
1691
|
+
});
|
|
1692
|
+
const assets = ASSET_SEEDS.map((a, i) => {
|
|
1693
|
+
const project = a.project === 0 ? p0 : p1;
|
|
1694
|
+
return add("Asset", 1226 + i, {
|
|
1695
|
+
code: a.code,
|
|
1696
|
+
cached_display_name: a.code,
|
|
1697
|
+
description: `${a.type} asset ${a.code}`,
|
|
1698
|
+
sg_status_list: pick(rng, SHOT_STATUSES),
|
|
1699
|
+
sg_asset_type: a.type,
|
|
1700
|
+
sg_build_days: 1 + Math.floor(rng() * 20),
|
|
1701
|
+
sg_complexity: Math.floor(rng() * 101),
|
|
1702
|
+
sg_due_date: isoDate(Math.floor(rng() * 150)),
|
|
1703
|
+
sg_published: rng() > .5,
|
|
1704
|
+
image: thumb(a.code),
|
|
1705
|
+
project: ref(project),
|
|
1706
|
+
shots: [],
|
|
1707
|
+
sequences: [],
|
|
1708
|
+
tasks: [],
|
|
1709
|
+
created_at: isoDateTime(-135 + i),
|
|
1710
|
+
updated_at: isoDateTime(-12 + i),
|
|
1711
|
+
created_by: ref(bot),
|
|
1712
|
+
updated_by: ref(bot)
|
|
1713
|
+
});
|
|
1714
|
+
});
|
|
1715
|
+
for (const shot of shots) {
|
|
1716
|
+
const projectId = shot.values["project"].id;
|
|
1717
|
+
const chosen = assets.filter((a) => a.values["project"].id === projectId).filter(() => rng() < .3);
|
|
1718
|
+
shot.values["assets"] = chosen.map(ref);
|
|
1719
|
+
for (const asset of chosen) asset.values["shots"].push(ref(shot));
|
|
1720
|
+
}
|
|
1721
|
+
const tasks = [];
|
|
1722
|
+
let taskId = 5700;
|
|
1723
|
+
const taskTargets = [...shots, ...assets];
|
|
1724
|
+
for (const target of taskTargets) {
|
|
1725
|
+
const applicable = steps.filter((s) => s.values["entity_type"] === target.type);
|
|
1726
|
+
const step = pick(rng, applicable.length > 0 ? applicable : steps);
|
|
1727
|
+
const content = String(step.values["code"]);
|
|
1728
|
+
const start = Math.floor(rng() * 90);
|
|
1729
|
+
const assignee = pick(rng, activeUsers);
|
|
1730
|
+
const task = add("Task", taskId, {
|
|
1731
|
+
content,
|
|
1732
|
+
cached_display_name: content,
|
|
1733
|
+
sg_description: `${content} on ${String(target.values["code"])}`,
|
|
1734
|
+
sg_status_list: pick(rng, TASK_STATUSES),
|
|
1735
|
+
start_date: isoDate(start),
|
|
1736
|
+
due_date: isoDate(start + 3 + Math.floor(rng() * 12)),
|
|
1737
|
+
duration: 480 * (1 + Math.floor(rng() * 5)),
|
|
1738
|
+
est_in_mins: 480 * (1 + Math.floor(rng() * 5)),
|
|
1739
|
+
time_logs_sum: 60 * Math.floor(rng() * 40),
|
|
1740
|
+
time_percent_of_est: Math.floor(rng() * 130),
|
|
1741
|
+
color: "pipeline_step",
|
|
1742
|
+
milestone: false,
|
|
1743
|
+
project: target.values["project"],
|
|
1744
|
+
entity: ref(target),
|
|
1745
|
+
step: ref(step),
|
|
1746
|
+
task_assignees: [ref(assignee)],
|
|
1747
|
+
task_reviewers: [],
|
|
1748
|
+
upstream_tasks: [],
|
|
1749
|
+
created_at: isoDateTime(-120 + taskId % 50),
|
|
1750
|
+
updated_at: isoDateTime(-8 + taskId % 6),
|
|
1751
|
+
created_by: ref(bot),
|
|
1752
|
+
updated_by: ref(bot)
|
|
1753
|
+
});
|
|
1754
|
+
target.values["tasks"].push(ref(task));
|
|
1755
|
+
tasks.push(task);
|
|
1756
|
+
taskId += 1;
|
|
1757
|
+
}
|
|
1758
|
+
const versions = [];
|
|
1759
|
+
let versionId = 17055;
|
|
1760
|
+
const versionCount = counts.versions ?? 60;
|
|
1761
|
+
for (let i = 0; i < versionCount; i += 1) {
|
|
1762
|
+
const target = i % 7 === 6 ? assets[i % assets.length] : shots[i % shots.length];
|
|
1763
|
+
const task = (target.values["tasks"] ?? [])[0];
|
|
1764
|
+
const stepName = task ? String(index.get(`Task:${task.id}`)?.values["content"] ?? "comp") : "comp";
|
|
1765
|
+
const revision = 1 + Math.floor(i / shots.length) * 3 + i % 3;
|
|
1766
|
+
const code = `${String(target.values["code"])}_${stepName.toLowerCase().replace(/\s+/g, "")}_v${String(revision).padStart(3, "0")}`;
|
|
1767
|
+
const first = 1001;
|
|
1768
|
+
const last = 1041 + Math.floor(rng() * 120);
|
|
1769
|
+
const version = add("Version", versionId, {
|
|
1770
|
+
code,
|
|
1771
|
+
cached_display_name: code,
|
|
1772
|
+
description: `Review submission for ${String(target.values["code"])}`,
|
|
1773
|
+
sg_department: i % 2 === 0 ? stepName : null,
|
|
1774
|
+
sg_status_list: pick(rng, VERSION_STATUSES),
|
|
1775
|
+
sg_version_type: pick(rng, VERSION_TYPES),
|
|
1776
|
+
sg_first_frame: first,
|
|
1777
|
+
sg_last_frame: last,
|
|
1778
|
+
frame_count: last - first + 1,
|
|
1779
|
+
sg_uploaded_movie_frame_rate: 24,
|
|
1780
|
+
sg_uploaded_movie_transcoding_status: 1,
|
|
1781
|
+
sg_path_to_frames: `/mnt/prod/${String(index.get(`Project:${target.values["project"].id}`).values["tank_name"])}/${String(target.values["code"])}/${code}.%04d.exr`,
|
|
1782
|
+
sg_path_to_movie: `/mnt/prod/mov/${code}.mov`,
|
|
1783
|
+
client_approved: rng() > .85,
|
|
1784
|
+
client_approved_at: null,
|
|
1785
|
+
image: i % 7 === 3 ? null : thumb(code),
|
|
1786
|
+
sg_uploaded_movie: null,
|
|
1787
|
+
sg_bar_color: pick(rng, BAR_COLORS),
|
|
1788
|
+
project: target.values["project"],
|
|
1789
|
+
entity: ref(target),
|
|
1790
|
+
sg_task: task ?? null,
|
|
1791
|
+
user: ref(pick(rng, activeUsers)),
|
|
1792
|
+
playlists: [],
|
|
1793
|
+
created_at: isoDateTime(-100 + i % 90, i),
|
|
1794
|
+
updated_at: isoDateTime(-3 + i % 3, i),
|
|
1795
|
+
created_by: ref(bot),
|
|
1796
|
+
updated_by: ref(bot)
|
|
1797
|
+
});
|
|
1798
|
+
versions.push(version);
|
|
1799
|
+
versionId += 1;
|
|
1800
|
+
}
|
|
1801
|
+
const notes = [];
|
|
1802
|
+
/** Each Reply with the day and minute it was written, for the event log below. */
|
|
1803
|
+
const replies = [];
|
|
1804
|
+
let noteId = 11030;
|
|
1805
|
+
let replyId = 610;
|
|
1806
|
+
let attachmentId = 2626;
|
|
1807
|
+
NOTE_SEEDS.forEach((seed, i) => {
|
|
1808
|
+
const target = versions[i === 3 ? 23 : i * 5] ?? shots[i];
|
|
1809
|
+
const project = target.values["project"];
|
|
1810
|
+
const author = users[seed.author];
|
|
1811
|
+
const addressed = users[(seed.author + 1) % users.length];
|
|
1812
|
+
const note = add("Note", noteId, {
|
|
1813
|
+
subject: seed.subject,
|
|
1814
|
+
content: seed.content,
|
|
1815
|
+
cached_display_name: `${seed.subject} - ${seed.content}`,
|
|
1816
|
+
sg_status_list: seed.status,
|
|
1817
|
+
sg_note_type: seed.noteType,
|
|
1818
|
+
read_by_current_user: seed.read,
|
|
1819
|
+
publish_status: "published",
|
|
1820
|
+
project,
|
|
1821
|
+
user: ref(author),
|
|
1822
|
+
note_links: [ref(target)],
|
|
1823
|
+
tasks: [],
|
|
1824
|
+
replies: [],
|
|
1825
|
+
attachments: [],
|
|
1826
|
+
addressings_to: [ref(addressed)],
|
|
1827
|
+
addressings_cc: [],
|
|
1828
|
+
created_at: isoDateTime(seed.day),
|
|
1829
|
+
updated_at: isoDateTime(seed.day),
|
|
1830
|
+
created_by: ref(author),
|
|
1831
|
+
updated_by: ref(author)
|
|
1832
|
+
});
|
|
1833
|
+
for (const file of seed.attachments) {
|
|
1834
|
+
const attachment = add("Attachment", attachmentId, {
|
|
1835
|
+
display_name: file.filename,
|
|
1836
|
+
cached_display_name: file.filename,
|
|
1837
|
+
description: null,
|
|
1838
|
+
original_fname: file.filename,
|
|
1839
|
+
filename: file.filename,
|
|
1840
|
+
file_extension: null,
|
|
1841
|
+
file_size: null,
|
|
1842
|
+
this_file: {
|
|
1843
|
+
url: `https://media.example.studio/${file.filename}`,
|
|
1844
|
+
name: file.filename,
|
|
1845
|
+
content_type: "image/png",
|
|
1846
|
+
link_type: "upload"
|
|
1847
|
+
},
|
|
1848
|
+
processing_status: null,
|
|
1849
|
+
sg_status_list: "na",
|
|
1850
|
+
project,
|
|
1851
|
+
attachment_links: [ref(note)],
|
|
1852
|
+
created_at: isoDateTime(seed.day, file.minutes * 60),
|
|
1853
|
+
updated_at: isoDateTime(seed.day, file.minutes * 60),
|
|
1854
|
+
created_by: ref(author),
|
|
1855
|
+
updated_by: ref(author)
|
|
1856
|
+
});
|
|
1857
|
+
note.values["attachments"].push(ref(attachment));
|
|
1858
|
+
attachmentId += 1;
|
|
1859
|
+
}
|
|
1860
|
+
for (const seeded of seed.replies) {
|
|
1861
|
+
const replyAuthor = users[seeded.author];
|
|
1862
|
+
const reply = add("Reply", replyId, {
|
|
1863
|
+
content: seeded.content,
|
|
1864
|
+
cached_display_name: seeded.content,
|
|
1865
|
+
entity: ref(note),
|
|
1866
|
+
user: ref(replyAuthor),
|
|
1867
|
+
publish_status: "published",
|
|
1868
|
+
created_at: isoDateTime(seed.day, seeded.minutes * 60),
|
|
1869
|
+
updated_at: isoDateTime(seed.day, seeded.minutes * 60)
|
|
1870
|
+
});
|
|
1871
|
+
note.values["replies"].push(ref(reply));
|
|
1872
|
+
replies.push({
|
|
1873
|
+
row: reply,
|
|
1874
|
+
day: seed.day,
|
|
1875
|
+
minutes: seeded.minutes
|
|
1876
|
+
});
|
|
1877
|
+
replyId += 1;
|
|
1878
|
+
}
|
|
1879
|
+
notes.push(note);
|
|
1880
|
+
noteId += 1;
|
|
1881
|
+
});
|
|
1882
|
+
const pending = [];
|
|
1883
|
+
const event = (day, seconds, values) => {
|
|
1884
|
+
pending.push({
|
|
1885
|
+
day,
|
|
1886
|
+
seconds,
|
|
1887
|
+
values
|
|
1888
|
+
});
|
|
1889
|
+
};
|
|
1890
|
+
const changed = (row, oldValue, day, seconds, user) => {
|
|
1891
|
+
const newValue = row.values["sg_status_list"];
|
|
1892
|
+
event(day, seconds, {
|
|
1893
|
+
event_type: `Shotgun_${row.type}_Change`,
|
|
1894
|
+
attribute_name: "sg_status_list",
|
|
1895
|
+
description: `${displayNameOf(user.values, "")} changed "Status" from "${oldValue}" to "${String(newValue)}" on ${row.type} ${displayNameOf(row.values, "")}`,
|
|
1896
|
+
meta: {
|
|
1897
|
+
type: "attribute_change",
|
|
1898
|
+
attribute_name: "sg_status_list",
|
|
1899
|
+
entity_type: row.type,
|
|
1900
|
+
entity_id: row.id,
|
|
1901
|
+
in_create: false,
|
|
1902
|
+
field_data_type: "status_list",
|
|
1903
|
+
old_value: oldValue,
|
|
1904
|
+
new_value: newValue,
|
|
1905
|
+
platform_id: null
|
|
1906
|
+
},
|
|
1907
|
+
entity: ref(row),
|
|
1908
|
+
project: row.values["project"],
|
|
1909
|
+
user: ref(user)
|
|
1910
|
+
});
|
|
1911
|
+
};
|
|
1912
|
+
const created = (row, day, seconds, user, extra = {}) => {
|
|
1913
|
+
event(day, seconds, {
|
|
1914
|
+
event_type: `Shotgun_${row.type}_New`,
|
|
1915
|
+
attribute_name: null,
|
|
1916
|
+
description: `${displayNameOf(user.values, "")} created ${row.type} ${displayNameOf(row.values, "")}`,
|
|
1917
|
+
meta: {
|
|
1918
|
+
type: "new_entity",
|
|
1919
|
+
entity_type: row.type,
|
|
1920
|
+
entity_id: row.id,
|
|
1921
|
+
...extra
|
|
1922
|
+
},
|
|
1923
|
+
entity: ref(row),
|
|
1924
|
+
project: row.values["project"] ?? null,
|
|
1925
|
+
user: ref(user)
|
|
1926
|
+
});
|
|
1927
|
+
};
|
|
1928
|
+
shots.slice(0, 6).forEach((shot, i) => changed(shot, "wtg", -9 + i, 3600 + i * 137, users[i % users.length]));
|
|
1929
|
+
versions.slice(0, 4).forEach((version, i) => changed(version, "rev", -5 + i, 7200 + i * 211, activeUsers[i % activeUsers.length]));
|
|
1930
|
+
tasks.slice(0, 3).forEach((task, i) => changed(task, "wtg", -7 + i, 5400 + i * 97, activeUsers[(i + 1) % activeUsers.length]));
|
|
1931
|
+
notes.forEach((note, i) => {
|
|
1932
|
+
const author = index.get(`HumanUser:${note.values["user"].id}`);
|
|
1933
|
+
created(note, NOTE_SEEDS[i]?.day ?? 0, 30, author);
|
|
1934
|
+
});
|
|
1935
|
+
for (const { row: reply, day, minutes } of replies) {
|
|
1936
|
+
const author = index.get(`HumanUser:${reply.values["user"].id}`);
|
|
1937
|
+
const note = index.get(`Note:${reply.values["entity"].id}`);
|
|
1938
|
+
event(day, minutes * 60, {
|
|
1939
|
+
event_type: "Shotgun_Reply_New",
|
|
1940
|
+
attribute_name: null,
|
|
1941
|
+
description: `${displayNameOf(author.values, "")} replied to Note ${displayNameOf(note.values, "")}`,
|
|
1942
|
+
meta: {
|
|
1943
|
+
type: "new_entity",
|
|
1944
|
+
entity_type: "Reply",
|
|
1945
|
+
entity_id: reply.id,
|
|
1946
|
+
content: reply.values["content"]
|
|
1947
|
+
},
|
|
1948
|
+
entity: ref(reply),
|
|
1949
|
+
project: note.values["project"],
|
|
1950
|
+
user: ref(author)
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
event(-30, 0, {
|
|
1954
|
+
event_type: "Shotgun_Shot_Change",
|
|
1955
|
+
attribute_name: "sg_status_list",
|
|
1956
|
+
description: "A shot that no longer exists changed \"Status\" from \"ip\" to \"omt\"",
|
|
1957
|
+
meta: {
|
|
1958
|
+
type: "attribute_change",
|
|
1959
|
+
attribute_name: "sg_status_list",
|
|
1960
|
+
entity_type: "Shot",
|
|
1961
|
+
entity_id: 9001,
|
|
1962
|
+
in_create: false,
|
|
1963
|
+
field_data_type: "status_list",
|
|
1964
|
+
old_value: "ip",
|
|
1965
|
+
new_value: "omt",
|
|
1966
|
+
platform_id: null
|
|
1967
|
+
},
|
|
1968
|
+
entity: null,
|
|
1969
|
+
project: ref(p0),
|
|
1970
|
+
user: ref(bot)
|
|
1971
|
+
});
|
|
1972
|
+
pending.sort((a, b) => a.day * 86400 + a.seconds - (b.day * 86400 + b.seconds));
|
|
1973
|
+
let eventId = 124e4;
|
|
1974
|
+
for (const entry of pending) {
|
|
1975
|
+
add("EventLogEntry", eventId, {
|
|
1976
|
+
cached_display_name: null,
|
|
1977
|
+
session_uuid: `7a1b2c3d-0000-4000-8000-${String(eventId).padStart(12, "0")}`,
|
|
1978
|
+
created_at: isoDateTime(entry.day, entry.seconds),
|
|
1979
|
+
...entry.values
|
|
1980
|
+
});
|
|
1981
|
+
eventId += eventId % 7 === 0 ? 3 : 1;
|
|
1982
|
+
}
|
|
1983
|
+
const follows = /* @__PURE__ */ new Map();
|
|
1984
|
+
const follow = (userRef, row) => {
|
|
1985
|
+
if (!userRef || userRef.type !== "HumanUser") return;
|
|
1986
|
+
const list = follows.get(userRef.id) ?? [];
|
|
1987
|
+
if (!list.some((r) => r.type === row.type && r.id === row.id)) list.push(ref(row));
|
|
1988
|
+
follows.set(userRef.id, list);
|
|
1989
|
+
};
|
|
1990
|
+
for (const note of notes) {
|
|
1991
|
+
follow(note.values["user"], note);
|
|
1992
|
+
for (const to of note.values["addressings_to"]) follow(to, note);
|
|
1993
|
+
}
|
|
1994
|
+
for (const { row: reply } of replies) {
|
|
1995
|
+
const note = index.get(`Note:${reply.values["entity"].id}`);
|
|
1996
|
+
if (note) follow(reply.values["user"], note);
|
|
1997
|
+
}
|
|
1998
|
+
for (const task of tasks) for (const assignee of task.values["task_assignees"]) follow(assignee, task);
|
|
1999
|
+
return {
|
|
2000
|
+
rows,
|
|
2001
|
+
index,
|
|
2002
|
+
follows
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
var MockClient = class {
|
|
2006
|
+
fixtures;
|
|
2007
|
+
latencyMs;
|
|
2008
|
+
clock;
|
|
2009
|
+
pendingFailure;
|
|
2010
|
+
constructor(options = {}) {
|
|
2011
|
+
this.fixtures = buildFixtures(options.seed ?? 1, options.counts ?? {});
|
|
2012
|
+
this.latencyMs = options.latencyMs ?? 0;
|
|
2013
|
+
this.clock = toClock(options.now);
|
|
2014
|
+
this.pendingFailure = options.failNext ?? null;
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Make the next call reject with an `SgApiError`, for demoing an error state.
|
|
2018
|
+
* Pass `null` to disarm one that was already set.
|
|
2019
|
+
*/
|
|
2020
|
+
failNext(failure = {}) {
|
|
2021
|
+
this.pendingFailure = failure;
|
|
2022
|
+
}
|
|
2023
|
+
/** Rows of a type, in id order. Handy for writing assertions against the fixtures. */
|
|
2024
|
+
rowsOf(entityType) {
|
|
2025
|
+
return (this.fixtures.rows.get(entityType) ?? []).map((r) => r.values);
|
|
2026
|
+
}
|
|
2027
|
+
async gate() {
|
|
2028
|
+
const failure = this.pendingFailure;
|
|
2029
|
+
this.pendingFailure = null;
|
|
2030
|
+
if (this.latencyMs > 0) await new Promise((resolve) => setTimeout(resolve, this.latencyMs));
|
|
2031
|
+
if (failure) {
|
|
2032
|
+
const status = failure.status ?? 500;
|
|
2033
|
+
throw new SgApiError(status, failure.body ?? null, failure.message ?? `Flow PT API error ${status}`);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
schemaOf(entityType) {
|
|
2037
|
+
const spec = SPECS[entityType];
|
|
2038
|
+
if (!spec) throw new SgApiError(404, null, `Entity type '${entityType}' does not exist.`);
|
|
2039
|
+
return spec;
|
|
2040
|
+
}
|
|
2041
|
+
async entityTypes() {
|
|
2042
|
+
await this.gate();
|
|
2043
|
+
return Object.keys(SPECS).map((name) => ({
|
|
2044
|
+
name,
|
|
2045
|
+
displayName: DISPLAY_NAMES[name] ?? name
|
|
2046
|
+
}));
|
|
2047
|
+
}
|
|
2048
|
+
async fields(entityType, projectId) {
|
|
2049
|
+
await this.gate();
|
|
2050
|
+
const spec = this.schemaOf(entityType);
|
|
2051
|
+
const hidden = projectId === void 0 ? void 0 : HIDDEN_VALUES[projectId] ?? {};
|
|
2052
|
+
const out = {};
|
|
2053
|
+
for (const [name, s] of Object.entries(spec)) {
|
|
2054
|
+
const field = {
|
|
2055
|
+
name,
|
|
2056
|
+
displayName: s.displayName,
|
|
2057
|
+
entityType,
|
|
2058
|
+
dataType: s.dataType,
|
|
2059
|
+
editable: s.editable ?? true,
|
|
2060
|
+
mandatory: s.mandatory ?? false,
|
|
2061
|
+
unique: s.unique ?? false
|
|
2062
|
+
};
|
|
2063
|
+
if (s.validTypes) field.validTypes = s.validTypes;
|
|
2064
|
+
if (s.validValues) field.validValues = s.validValues;
|
|
2065
|
+
if (s.displayValues) field.displayValues = s.displayValues;
|
|
2066
|
+
if (s.defaultValue !== void 0) field.defaultValue = s.defaultValue;
|
|
2067
|
+
if (s.description) field.description = s.description;
|
|
2068
|
+
if (hidden && (s.dataType === "status_list" || s.dataType === "list")) field.hiddenValues = hidden[`${entityType}.${name}`] ?? [];
|
|
2069
|
+
out[name] = {
|
|
2070
|
+
...field,
|
|
2071
|
+
...fieldSchemaOverride(entityType, name)
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
return out;
|
|
2075
|
+
}
|
|
2076
|
+
async fieldWithProject(entityType, field, projectId) {
|
|
2077
|
+
const one = (await this.fields(entityType, projectId))[field];
|
|
2078
|
+
if (!one) throw new SgApiError(404, null, `Field '${entityType}.${field}' does not exist.`);
|
|
2079
|
+
return one;
|
|
2080
|
+
}
|
|
2081
|
+
async search(entityType, options) {
|
|
2082
|
+
await this.gate();
|
|
2083
|
+
const spec = this.schemaOf(entityType);
|
|
2084
|
+
const matched = (this.fixtures.rows.get(entityType) ?? []).filter((row) => this.matchGroup(row, entityType, options.filters ?? null));
|
|
2085
|
+
const sorted = this.applySort(matched, entityType, options.sort);
|
|
2086
|
+
const size = options.page?.size ?? 50;
|
|
2087
|
+
const start = ((options.page?.number ?? 1) - 1) * size;
|
|
2088
|
+
const page = sorted.slice(start, start + size);
|
|
2089
|
+
return {
|
|
2090
|
+
data: page.map((row) => this.project(row, spec, options.fields)),
|
|
2091
|
+
hasMore: page.length === size
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
async textSearch(text, entityTypes, page) {
|
|
2095
|
+
await this.gate();
|
|
2096
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
2097
|
+
if (words.length === 0) throw new SgApiError(400, { page: null }, "text must be filled");
|
|
2098
|
+
const size = page?.size ?? 25;
|
|
2099
|
+
if (size > 25) throw new SgApiError(400, null, "size must be less than 25");
|
|
2100
|
+
if (size < 1) throw new SgApiError(400, null, "size must be greater than 0");
|
|
2101
|
+
const number = page?.number ?? 1;
|
|
2102
|
+
if (number < 1) throw new SgApiError(400, null, "number must be greater than 0");
|
|
2103
|
+
const needles = words.map((w) => w.toLowerCase());
|
|
2104
|
+
const hits = [];
|
|
2105
|
+
for (const [type, filter] of Object.entries(entityTypes)) {
|
|
2106
|
+
const group = {
|
|
2107
|
+
logical_operator: "and",
|
|
2108
|
+
conditions: toFilterArray(filter)
|
|
2109
|
+
};
|
|
2110
|
+
for (const row of this.fixtures.rows.get(type) ?? []) {
|
|
2111
|
+
if (!this.matchGroup(row, type, group)) continue;
|
|
2112
|
+
const name = displayNameOf(row.values, `#${row.id}`);
|
|
2113
|
+
const links = this.linkedPair(row);
|
|
2114
|
+
const haystack = `${name} ${links[1]}`.toLowerCase();
|
|
2115
|
+
if (!needles.every((w) => haystack.includes(w))) continue;
|
|
2116
|
+
hits.push({
|
|
2117
|
+
type,
|
|
2118
|
+
id: row.id,
|
|
2119
|
+
name,
|
|
2120
|
+
links,
|
|
2121
|
+
status: row.values["sg_status_list"] ?? row.values["sg_status"] ?? null,
|
|
2122
|
+
sortName: name
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
hits.sort((a, b) => a.sortName.length - b.sortName.length || (a.sortName < b.sortName ? -1 : a.sortName > b.sortName ? 1 : a.id - b.id));
|
|
2127
|
+
return hits.slice((number - 1) * size, (number - 1) * size + size).map(({ sortName: _sortName, ...row }) => row);
|
|
2128
|
+
}
|
|
2129
|
+
async update(entityType, id, patch) {
|
|
2130
|
+
await this.gate();
|
|
2131
|
+
const spec = this.schemaOf(entityType);
|
|
2132
|
+
const row = this.fixtures.index.get(`${entityType}:${id}`);
|
|
2133
|
+
if (!row) throw new SgApiError(404, null, `Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2134
|
+
for (const [name, value] of Object.entries(patch)) {
|
|
2135
|
+
const field = spec[name];
|
|
2136
|
+
if (!field) throw new SgApiError(400, null, `API update() ${entityType}.${name} doesn't exist.`);
|
|
2137
|
+
if (field.createOnly) throw new SgApiError(400, null, `API update() ${entityType}.${name} is editable on create only.`);
|
|
2138
|
+
if (field.editable === false) throw new SgApiError(400, null, `API update() ${entityType}.${name} is read only.`);
|
|
2139
|
+
this.checkLink("update", entityType, name, field, value);
|
|
2140
|
+
row.values[name] = field.dataType === "text" && value === "" ? null : value;
|
|
2141
|
+
}
|
|
2142
|
+
if (spec["updated_at"] && Object.keys(patch).length > 0) row.values["updated_at"] = isoDateTime(0);
|
|
2143
|
+
return this.project(row, spec);
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Counts without paging rows.
|
|
2147
|
+
*
|
|
2148
|
+
* `count` and the numeric aggregates are modelled; the rest of the vocabulary the
|
|
2149
|
+
* endpoint prints is not. A grouping returns one group per distinct value with the
|
|
2150
|
+
* empties under a `''` group, `group_value` is what the grouping was computed on and
|
|
2151
|
+
* `group_name` the server's render of it, and one type per field per call wins
|
|
2152
|
+
* (020_summarize).
|
|
2153
|
+
*/
|
|
2154
|
+
async summarize(entityType, options = {}) {
|
|
2155
|
+
await this.gate();
|
|
2156
|
+
this.schemaOf(entityType);
|
|
2157
|
+
const matched = (this.fixtures.rows.get(entityType) ?? []).filter((row) => this.matchGroup(row, entityType, options.filters ?? null));
|
|
2158
|
+
const fields = options.summaryFields ?? [{
|
|
2159
|
+
field: "id",
|
|
2160
|
+
type: "count"
|
|
2161
|
+
}];
|
|
2162
|
+
const summarize = (rows) => {
|
|
2163
|
+
const out = {};
|
|
2164
|
+
for (const f of fields) {
|
|
2165
|
+
const values = rows.map((r) => this.walk(r, f.field, false)[0] ?? null).filter((v) => v !== null);
|
|
2166
|
+
const numbers = values.map(Number).filter((n) => !Number.isNaN(n));
|
|
2167
|
+
switch (f.type) {
|
|
2168
|
+
case "count":
|
|
2169
|
+
case "record_count":
|
|
2170
|
+
out[f.field] = f.field === "id" || f.type === "record_count" ? rows.length : values.length;
|
|
2171
|
+
break;
|
|
2172
|
+
case "sum":
|
|
2173
|
+
out[f.field] = numbers.reduce((a, b) => a + b, 0);
|
|
2174
|
+
break;
|
|
2175
|
+
case "maximum":
|
|
2176
|
+
out[f.field] = numbers.length > 0 ? Math.max(...numbers) : 0;
|
|
2177
|
+
break;
|
|
2178
|
+
case "minimum":
|
|
2179
|
+
out[f.field] = numbers.length > 0 ? Math.min(...numbers) : 0;
|
|
2180
|
+
break;
|
|
2181
|
+
case "average": out[f.field] = numbers.length > 0 ? numbers.reduce((a, b) => a + b, 0) / numbers.length : 0;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return out;
|
|
2185
|
+
};
|
|
2186
|
+
const grouping = options.grouping?.[0];
|
|
2187
|
+
if (!grouping) return {
|
|
2188
|
+
summaries: summarize(matched),
|
|
2189
|
+
groups: []
|
|
2190
|
+
};
|
|
2191
|
+
this.refuseGrouping(entityType, grouping.field);
|
|
2192
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
2193
|
+
const put = (value, row) => {
|
|
2194
|
+
const key = value === null || value === void 0 ? "" : JSON.stringify(value);
|
|
2195
|
+
const bucket = buckets.get(key);
|
|
2196
|
+
if (bucket) bucket.rows.push(row);
|
|
2197
|
+
else buckets.set(key, {
|
|
2198
|
+
value,
|
|
2199
|
+
rows: [row]
|
|
2200
|
+
});
|
|
2201
|
+
};
|
|
2202
|
+
for (const row of matched) {
|
|
2203
|
+
const raw = this.walk(row, grouping.field, false)[0] ?? null;
|
|
2204
|
+
put(Array.isArray(raw) ? raw.length === 0 ? null : raw.map((one) => this.groupValueOf(one)) : this.groupValueOf(raw), row);
|
|
2205
|
+
}
|
|
2206
|
+
const groups = [...buckets.entries()].map(([, bucket]) => ({
|
|
2207
|
+
groupName: bucket.value === null ? "" : Array.isArray(bucket.value) ? bucket.value.map(groupLabel).join(", ") : groupLabel(bucket.value),
|
|
2208
|
+
groupValue: bucket.value,
|
|
2209
|
+
summaries: summarize(bucket.rows)
|
|
2210
|
+
}));
|
|
2211
|
+
groups.sort((a, b) => a.groupName < b.groupName ? -1 : a.groupName > b.groupName ? 1 : 0);
|
|
2212
|
+
if (grouping.direction === "desc") groups.reverse();
|
|
2213
|
+
return {
|
|
2214
|
+
summaries: summarize(matched),
|
|
2215
|
+
groups
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
/** An entity group's value is the reference with its name and `valid` (020_summarize); a code is itself. */
|
|
2219
|
+
groupValueOf(value) {
|
|
2220
|
+
if (value === null || typeof value !== "object" || !("type" in value)) return value;
|
|
2221
|
+
return {
|
|
2222
|
+
...this.decorate(value),
|
|
2223
|
+
valid: "valid"
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* A field the server cannot group is 400 `Grouping is not allowed for field
|
|
2228
|
+
* <Type>.<field>.` on `image` and `summary` (field_types/image, field_types/summary),
|
|
2229
|
+
* and a `pivot_column` is 500 (field_types/pivot_column). `Note.read_by_current_user`,
|
|
2230
|
+
* the per-person read state, is refused the same way; the corpus has not measured it.
|
|
2231
|
+
*/
|
|
2232
|
+
refuseGrouping(entityType, field) {
|
|
2233
|
+
const dataType = SPECS[entityType]?.[field]?.dataType;
|
|
2234
|
+
if (dataType === "pivot_column") throw new SgApiError(500, null, "Shotgun Server Error");
|
|
2235
|
+
if (dataType === "image" || dataType === "summary" || entityType === "Note" && field === "read_by_current_user") throw new SgApiError(400, null, `Grouping is not allowed for field ${entityType}.${field}.`);
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* One level of the navigation tree.
|
|
2239
|
+
*
|
|
2240
|
+
* The shape is post_hierarchy_expand's: a node with `label`, `ref`, `path` and one
|
|
2241
|
+
* level of `children`. Which levels a project has is the site's own navigation
|
|
2242
|
+
* configuration and not a fixed hierarchy - the probed site's Shot path runs through
|
|
2243
|
+
* the field name `sg_sequence` (post_hierarchy_search) - so this fixture offers the
|
|
2244
|
+
* two branches that configuration draws for a stock project: Shots under their
|
|
2245
|
+
* Sequence, and Assets flat.
|
|
2246
|
+
*/
|
|
2247
|
+
async hierarchyExpand(path) {
|
|
2248
|
+
await this.gate();
|
|
2249
|
+
const parts = path.split("/").filter(Boolean);
|
|
2250
|
+
if (parts[0] !== "Project" || parts.length < 2) throw new SgApiError(400, null, `Unexpected result looking for project: ${parts[1] ?? path}: 0 found.`);
|
|
2251
|
+
const projectId = Number(parts[1]);
|
|
2252
|
+
const project = this.fixtures.index.get(`Project:${projectId}`);
|
|
2253
|
+
if (!project) throw new SgApiError(400, null, `Unexpected result looking for project: ${String(parts[1])}: 0 found.`);
|
|
2254
|
+
const rest = parts.slice(2);
|
|
2255
|
+
const above = parts.slice(0, -1).join("/");
|
|
2256
|
+
const parentPath = above === "Project" ? "/" : `/${above}`;
|
|
2257
|
+
const node = (label, ref, own, children, hasChildren) => ({
|
|
2258
|
+
label,
|
|
2259
|
+
ref,
|
|
2260
|
+
path: own,
|
|
2261
|
+
parentPath: own === path ? parentPath : null,
|
|
2262
|
+
hasChildren: hasChildren ?? children.length > 0,
|
|
2263
|
+
children
|
|
2264
|
+
});
|
|
2265
|
+
const tasksOf = (row) => (row.values["tasks"] ?? []).map((t) => this.fixtures.index.get(`Task:${t.id}`)).filter((t) => t !== void 0);
|
|
2266
|
+
const leaf = (row, own) => node(displayNameOf(row.values, `#${row.id}`), {
|
|
2267
|
+
kind: "entity",
|
|
2268
|
+
value: {
|
|
2269
|
+
type: row.type,
|
|
2270
|
+
id: row.id
|
|
2271
|
+
}
|
|
2272
|
+
}, own, [], tasksOf(row).length > 0);
|
|
2273
|
+
const rowsOf = (type) => (this.fixtures.rows.get(type) ?? []).filter((r) => r.values["project"]?.id === projectId);
|
|
2274
|
+
if (rest.length === 0) return node(String(project.values["name"]), {
|
|
2275
|
+
kind: "entity",
|
|
2276
|
+
value: {
|
|
2277
|
+
type: "Project",
|
|
2278
|
+
id: projectId
|
|
2279
|
+
}
|
|
2280
|
+
}, path, [node("Assets", {
|
|
2281
|
+
kind: "entity_type",
|
|
2282
|
+
value: "Asset"
|
|
2283
|
+
}, `${path}/Asset`, [], rowsOf("Asset").length > 0), node("Shots", {
|
|
2284
|
+
kind: "entity_type",
|
|
2285
|
+
value: "Shot"
|
|
2286
|
+
}, `${path}/Shot`, [], rowsOf("Shot").length > 0)]);
|
|
2287
|
+
if (rest.length === 1 && rest[0] === "Asset") return node("Assets", {
|
|
2288
|
+
kind: "entity_type",
|
|
2289
|
+
value: "Asset"
|
|
2290
|
+
}, path, rowsOf("Asset").map((r) => leaf(r, `${path}/id/${r.id}`)));
|
|
2291
|
+
/** A child standing for a level with nothing in it. It carries no path of its own. */
|
|
2292
|
+
const noRows = (label) => node(label, {
|
|
2293
|
+
kind: "empty",
|
|
2294
|
+
value: null
|
|
2295
|
+
}, path, [], false);
|
|
2296
|
+
const looseShots = () => rowsOf("Shot").filter((r) => r.values["sg_sequence"] === null);
|
|
2297
|
+
if (rest.length === 1 && rest[0] === "Shot") {
|
|
2298
|
+
const groups = rowsOf("Sequence").map((seq) => node(String(seq.values["code"]), {
|
|
2299
|
+
kind: "entity",
|
|
2300
|
+
value: {
|
|
2301
|
+
type: "Sequence",
|
|
2302
|
+
id: seq.id
|
|
2303
|
+
}
|
|
2304
|
+
}, `${path}/sg_sequence/Sequence/${seq.id}`, [], seq.values["shots"].length > 0));
|
|
2305
|
+
if (groups.length === 0) return node("Shots", {
|
|
2306
|
+
kind: "entity_type",
|
|
2307
|
+
value: "Shot"
|
|
2308
|
+
}, path, [noRows("No Shots")]);
|
|
2309
|
+
if (looseShots().length > 0) groups.push(node("Shots with no Sequence", {
|
|
2310
|
+
kind: "entity_type",
|
|
2311
|
+
value: "Shot"
|
|
2312
|
+
}, `${path}/sg_sequence/Sequence/__none__`, [], true));
|
|
2313
|
+
return node("Shots", {
|
|
2314
|
+
kind: "entity_type",
|
|
2315
|
+
value: "Shot"
|
|
2316
|
+
}, path, groups);
|
|
2317
|
+
}
|
|
2318
|
+
if (rest[0] === "Shot" && rest[1] === "sg_sequence" && rest[rest.length - 1] === "__none__") {
|
|
2319
|
+
const loose = looseShots();
|
|
2320
|
+
return node("Shots with no Sequence", {
|
|
2321
|
+
kind: "entity_type",
|
|
2322
|
+
value: "Shot"
|
|
2323
|
+
}, path, loose.length > 0 ? loose.map((r) => leaf(r, `${path}/id/${r.id}`)) : [noRows("No Shots")]);
|
|
2324
|
+
}
|
|
2325
|
+
if (rest[0] === "Shot" && rest.length > 1 && rest[1] !== "sg_sequence" && rest[1] !== "id") throw new SgApiError(400, null, `Unexpected field name in path: ${String(rest[1])} (expecting sg_sequence)`);
|
|
2326
|
+
if (rest.length === 4 && rest[0] === "Shot" && rest[1] === "sg_sequence" && rest[2] === "Sequence") {
|
|
2327
|
+
const seq = this.fixtures.index.get(`Sequence:${Number(rest[3])}`);
|
|
2328
|
+
if (!seq) throw new SgApiError(400, null, `Unexpected result looking for project: ${String(rest[3])}: 0 found.`);
|
|
2329
|
+
const shots = seq.values["shots"].map((r) => this.fixtures.index.get(`Shot:${r.id}`)).filter((r) => r !== void 0);
|
|
2330
|
+
return node(String(seq.values["code"]), {
|
|
2331
|
+
kind: "entity",
|
|
2332
|
+
value: {
|
|
2333
|
+
type: "Sequence",
|
|
2334
|
+
id: seq.id
|
|
2335
|
+
}
|
|
2336
|
+
}, path, shots.map((r) => leaf(r, `${path}/id/${r.id}`)));
|
|
2337
|
+
}
|
|
2338
|
+
const owner = rest[rest.length - 2] === "id" ? this.rowAtPath(rest) : null;
|
|
2339
|
+
if (owner) {
|
|
2340
|
+
const tasks = tasksOf(owner);
|
|
2341
|
+
return node(displayNameOf(owner.values, `#${owner.id}`), {
|
|
2342
|
+
kind: "entity",
|
|
2343
|
+
value: {
|
|
2344
|
+
type: owner.type,
|
|
2345
|
+
id: owner.id
|
|
2346
|
+
}
|
|
2347
|
+
}, path, tasks.length > 0 ? [node("Tasks", {
|
|
2348
|
+
kind: "entity_type",
|
|
2349
|
+
value: "Task"
|
|
2350
|
+
}, `${path}/Task`, [], true)] : []);
|
|
2351
|
+
}
|
|
2352
|
+
if (rest[rest.length - 1] === "Task") {
|
|
2353
|
+
const holder = this.rowAtPath(rest.slice(0, -1));
|
|
2354
|
+
return node("Tasks", {
|
|
2355
|
+
kind: "entity_type",
|
|
2356
|
+
value: "Task"
|
|
2357
|
+
}, path, (holder ? tasksOf(holder) : []).map((t) => leaf(t, `${path}/id/${t.id}`)));
|
|
2358
|
+
}
|
|
2359
|
+
return node(path.split("/").pop() ?? "", {
|
|
2360
|
+
kind: "empty",
|
|
2361
|
+
value: null
|
|
2362
|
+
}, path, [], false);
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Where a row sits in the tree. The endpoint takes an entity and answers its
|
|
2366
|
+
* breadcrumb; it does not match words (post_hierarchy_search).
|
|
2367
|
+
*/
|
|
2368
|
+
async hierarchySearch(rootPath, entity) {
|
|
2369
|
+
await this.gate();
|
|
2370
|
+
const incremental = this.pathTo(entity);
|
|
2371
|
+
const self = incremental[incremental.length - 1];
|
|
2372
|
+
if (self === void 0 || !self.startsWith(rootPath)) return [];
|
|
2373
|
+
const labels = incremental.slice(1, -1).map((p) => this.labelAtPath(p));
|
|
2374
|
+
const row = this.fixtures.index.get(`${entity.type}:${entity.id}`);
|
|
2375
|
+
return [{
|
|
2376
|
+
label: row ? displayNameOf(row.values, `#${entity.id}`) : `${entity.type} #${entity.id}`,
|
|
2377
|
+
pathLabel: labels.join(" > "),
|
|
2378
|
+
incrementalPath: incremental,
|
|
2379
|
+
ref: {
|
|
2380
|
+
type: entity.type,
|
|
2381
|
+
id: entity.id
|
|
2382
|
+
},
|
|
2383
|
+
projectId: row ? row.values["project"]?.id ?? row.id : null
|
|
2384
|
+
}];
|
|
2385
|
+
}
|
|
2386
|
+
/** What a level of the tree is called, without opening it. */
|
|
2387
|
+
labelAtPath(path) {
|
|
2388
|
+
const rest = path.split("/").filter(Boolean).slice(2);
|
|
2389
|
+
const last = rest[rest.length - 1];
|
|
2390
|
+
if (last === "Asset") return "Assets";
|
|
2391
|
+
if (last === "Shot") return "Shots";
|
|
2392
|
+
if (last === "Task") return "Tasks";
|
|
2393
|
+
if (last === "__none__") return "Shots with no Sequence";
|
|
2394
|
+
if (rest[rest.length - 2] === "Sequence") {
|
|
2395
|
+
const sequence = this.fixtures.index.get(`Sequence:${Number(last)}`);
|
|
2396
|
+
return sequence ? displayNameOf(sequence.values, `#${sequence.id}`) : "";
|
|
2397
|
+
}
|
|
2398
|
+
const row = this.rowAtPath(rest);
|
|
2399
|
+
return row ? displayNameOf(row.values, `#${row.id}`) : "";
|
|
2400
|
+
}
|
|
2401
|
+
/** The row a `.../id/<n>` path segment names, from the type the path last named. */
|
|
2402
|
+
rowAtPath(rest) {
|
|
2403
|
+
const id = Number(rest[rest.length - 1]);
|
|
2404
|
+
if (!Number.isInteger(id)) return null;
|
|
2405
|
+
const type = rest.includes("Task") ? "Task" : rest[0] === "Asset" ? "Asset" : "Shot";
|
|
2406
|
+
return this.fixtures.index.get(`${type}:${id}`) ?? null;
|
|
2407
|
+
}
|
|
2408
|
+
/** The breadcrumb to a row, root first, the row itself last. Empty when it is not in the tree. */
|
|
2409
|
+
pathTo(entity) {
|
|
2410
|
+
const row = this.fixtures.index.get(`${entity.type}:${entity.id}`);
|
|
2411
|
+
if (!row) return [];
|
|
2412
|
+
if (entity.type === "Project") return [`/Project/${entity.id}`];
|
|
2413
|
+
const projectId = row.values["project"]?.id;
|
|
2414
|
+
if (projectId === void 0) return [];
|
|
2415
|
+
const root = `/Project/${projectId}`;
|
|
2416
|
+
switch (entity.type) {
|
|
2417
|
+
case "Sequence": return [
|
|
2418
|
+
root,
|
|
2419
|
+
`${root}/Shot`,
|
|
2420
|
+
`${root}/Shot/sg_sequence/Sequence/${entity.id}`
|
|
2421
|
+
];
|
|
2422
|
+
case "Asset": return [
|
|
2423
|
+
root,
|
|
2424
|
+
`${root}/Asset`,
|
|
2425
|
+
`${root}/Asset/id/${entity.id}`
|
|
2426
|
+
];
|
|
2427
|
+
case "Shot": {
|
|
2428
|
+
const sequence = row.values["sg_sequence"];
|
|
2429
|
+
const above = sequence ? this.pathTo({
|
|
2430
|
+
type: "Sequence",
|
|
2431
|
+
id: sequence.id
|
|
2432
|
+
}) : [
|
|
2433
|
+
root,
|
|
2434
|
+
`${root}/Shot`,
|
|
2435
|
+
`${root}/Shot/sg_sequence/__none__`
|
|
2436
|
+
];
|
|
2437
|
+
if (above.length === 0) return [];
|
|
2438
|
+
return [...above, `${above[above.length - 1]}/id/${entity.id}`];
|
|
2439
|
+
}
|
|
2440
|
+
case "Task": {
|
|
2441
|
+
const owner = row.values["entity"];
|
|
2442
|
+
if (!owner) return [];
|
|
2443
|
+
const above = this.pathTo(owner);
|
|
2444
|
+
if (above.length === 0) return [];
|
|
2445
|
+
const ownerPath = above[above.length - 1];
|
|
2446
|
+
return [
|
|
2447
|
+
...above,
|
|
2448
|
+
`${ownerPath}/Task`,
|
|
2449
|
+
`${ownerPath}/Task/id/${entity.id}`
|
|
2450
|
+
];
|
|
2451
|
+
}
|
|
2452
|
+
default: return [];
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
async statuses() {
|
|
2456
|
+
await this.gate();
|
|
2457
|
+
return (this.fixtures.rows.get("Status") ?? []).map((row) => {
|
|
2458
|
+
const iconRef = row.values["icon"];
|
|
2459
|
+
const icon = iconRef ? this.fixtures.index.get(`Icon:${iconRef.id}`) : void 0;
|
|
2460
|
+
return {
|
|
2461
|
+
id: row.id,
|
|
2462
|
+
code: String(row.values["code"] ?? ""),
|
|
2463
|
+
name: String(row.values["name"] ?? ""),
|
|
2464
|
+
bgColor: row.values["bg_color"] ?? null,
|
|
2465
|
+
icon: icon ? toStatusIcon(icon.values) : null
|
|
2466
|
+
};
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Create one row and answer it.
|
|
2471
|
+
*
|
|
2472
|
+
* `project` is the whole contract on a project-scoped type, and the schema's
|
|
2473
|
+
* `mandatory` flags are not it: the identity field is optional and the server
|
|
2474
|
+
* fills it, except on a Note, which stays titleless (012_create_version,
|
|
2475
|
+
* entity_types/Note). Nothing is unique, so two identical creates make two rows.
|
|
2476
|
+
*/
|
|
2477
|
+
async create(entityType, body) {
|
|
2478
|
+
await this.gate();
|
|
2479
|
+
const spec = this.schemaOf(entityType);
|
|
2480
|
+
if (spec["project"] && body["project"] === void 0) throw new SgApiError(400, null, `API create() missing 'project' attribute: ${JSON.stringify(body)}`);
|
|
2481
|
+
const identity = IDENTITY_FIELD[entityType];
|
|
2482
|
+
for (const [name, value] of Object.entries(body)) {
|
|
2483
|
+
const field = spec[name];
|
|
2484
|
+
if (!field) throw new SgApiError(400, null, `API create() ${entityType}.${name} doesn't exist.`);
|
|
2485
|
+
if (field.editable === false && !field.createOnly) throw new SgApiError(400, null, `API create() ${entityType}.${name} is read only.`);
|
|
2486
|
+
this.checkLink("create", entityType, name, field, value);
|
|
2487
|
+
if (name === identity && value === "") throw new SgApiError(400, null, `Create failed for [${entityType}]: Cannot set identifier field to empty. (${entityType})`);
|
|
2488
|
+
}
|
|
2489
|
+
const id = this.nextId(entityType);
|
|
2490
|
+
const values = {};
|
|
2491
|
+
for (const [name, field] of Object.entries(spec)) values[name] = field.dataType === "multi_entity" ? [] : field.defaultValue ?? null;
|
|
2492
|
+
const author = this.fixtures.rows.get("ApiUser")?.[0];
|
|
2493
|
+
const authored = author ? ref(author) : null;
|
|
2494
|
+
if (spec["created_by"]) values["created_by"] = authored;
|
|
2495
|
+
if (spec["updated_by"]) values["updated_by"] = authored;
|
|
2496
|
+
if (spec["user"]) values["user"] = authored;
|
|
2497
|
+
if (spec["read_by_current_user"]) values["read_by_current_user"] = "unread";
|
|
2498
|
+
if (spec["publish_status"]) values["publish_status"] = "published";
|
|
2499
|
+
values["created_at"] = isoDateTime(0);
|
|
2500
|
+
if (spec["updated_at"]) values["updated_at"] = isoDateTime(0);
|
|
2501
|
+
Object.assign(values, body, { id });
|
|
2502
|
+
if (identity && values[identity] === null && GENERATED_IDENTITY.has(entityType)) values[identity] = `New ${DISPLAY_NAMES[entityType] ?? entityType} ${id}`;
|
|
2503
|
+
values["cached_display_name"] = entityType === "Note" ? [values["subject"], values["content"]].filter(Boolean).join(" - ") : displayNameOf({
|
|
2504
|
+
...values,
|
|
2505
|
+
cached_display_name: null
|
|
2506
|
+
}, "");
|
|
2507
|
+
const row = {
|
|
2508
|
+
type: entityType,
|
|
2509
|
+
id,
|
|
2510
|
+
values
|
|
2511
|
+
};
|
|
2512
|
+
const rows = this.fixtures.rows.get(entityType);
|
|
2513
|
+
if (rows) rows.push(row);
|
|
2514
|
+
else this.fixtures.rows.set(entityType, [row]);
|
|
2515
|
+
this.fixtures.index.set(`${entityType}:${id}`, row);
|
|
2516
|
+
this.linkBack(row);
|
|
2517
|
+
return this.project(row, spec);
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* Put a file on a row, as the three-call handshake leaves the site.
|
|
2521
|
+
*
|
|
2522
|
+
* The field in the path picks the kind: `image` a Thumbnail, another field an
|
|
2523
|
+
* Attachment on it, no field a generic Attachment on `attachment_links`
|
|
2524
|
+
* (recipes/001). The mock moves no bytes, so there is no `ETag` to give back.
|
|
2525
|
+
*/
|
|
2526
|
+
async upload(entityType, id, file) {
|
|
2527
|
+
await this.gate();
|
|
2528
|
+
const spec = this.schemaOf(entityType);
|
|
2529
|
+
const target = this.fixtures.index.get(`${entityType}:${id}`);
|
|
2530
|
+
if (!target) throw new SgApiError(404, null, `Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2531
|
+
if (!file.filename) throw new SgApiError(400, { filename: ["filename is missing"] }, "Request Parameters invalid.");
|
|
2532
|
+
if (file.field !== void 0 && !spec[file.field]) throw new SgApiError(404, null, `Field '${entityType}.${file.field}' does not exist.`);
|
|
2533
|
+
const attachmentId = this.nextId("Attachment");
|
|
2534
|
+
const author = this.fixtures.rows.get("ApiUser")?.[0];
|
|
2535
|
+
const attachment = {
|
|
2536
|
+
type: "Attachment",
|
|
2537
|
+
id: attachmentId,
|
|
2538
|
+
values: {
|
|
2539
|
+
id: attachmentId,
|
|
2540
|
+
display_name: file.filename,
|
|
2541
|
+
cached_display_name: file.filename,
|
|
2542
|
+
description: null,
|
|
2543
|
+
original_fname: file.filename,
|
|
2544
|
+
filename: file.filename,
|
|
2545
|
+
file_extension: null,
|
|
2546
|
+
file_size: null,
|
|
2547
|
+
this_file: {
|
|
2548
|
+
url: `https://media.example.studio/${file.filename}`,
|
|
2549
|
+
name: file.filename,
|
|
2550
|
+
content_type: "application/octet-stream",
|
|
2551
|
+
link_type: "upload"
|
|
2552
|
+
},
|
|
2553
|
+
processing_status: "thumbnail_pending_us",
|
|
2554
|
+
sg_status_list: "na",
|
|
2555
|
+
project: target.values["project"] ?? null,
|
|
2556
|
+
attachment_links: [{
|
|
2557
|
+
type: entityType,
|
|
2558
|
+
id
|
|
2559
|
+
}],
|
|
2560
|
+
created_at: isoDateTime(0),
|
|
2561
|
+
updated_at: isoDateTime(0),
|
|
2562
|
+
created_by: author ? ref(author) : null,
|
|
2563
|
+
updated_by: author ? ref(author) : null
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
const attachments = this.fixtures.rows.get("Attachment");
|
|
2567
|
+
if (attachments) attachments.push(attachment);
|
|
2568
|
+
else this.fixtures.rows.set("Attachment", [attachment]);
|
|
2569
|
+
this.fixtures.index.set(`Attachment:${attachmentId}`, attachment);
|
|
2570
|
+
const linkField = file.field ?? (spec["attachments"] ? "attachments" : void 0);
|
|
2571
|
+
const field = linkField === void 0 ? void 0 : spec[linkField];
|
|
2572
|
+
if (linkField !== void 0 && field) {
|
|
2573
|
+
if (field.dataType === "multi_entity") target.values[linkField].push(ref(attachment));
|
|
2574
|
+
else if (linkField === "image") target.values[linkField] = `${MOCK_SITE_URL}/images/status/transient/thumbnail_pending.png`;
|
|
2575
|
+
else target.values[linkField] = String(attachment.values["this_file"].url);
|
|
2576
|
+
}
|
|
2577
|
+
const uploadType = file.field === "image" ? "Thumbnail" : "Attachment";
|
|
2578
|
+
return {
|
|
2579
|
+
uploadType,
|
|
2580
|
+
uploadInfo: {
|
|
2581
|
+
timestamp: isoDateTime(0),
|
|
2582
|
+
upload_type: uploadType,
|
|
2583
|
+
upload_id: null,
|
|
2584
|
+
storage_service: "s3",
|
|
2585
|
+
original_filename: file.filename,
|
|
2586
|
+
multipart_upload: false
|
|
2587
|
+
},
|
|
2588
|
+
etag: null
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* An entity link is a `{type, id}` hash. A bare id is refused naming the class it
|
|
2593
|
+
* got (entity_types/Reply, field_types/entity) and a hash with no `type` naming the
|
|
2594
|
+
* missing key (field_types/entity).
|
|
2595
|
+
*/
|
|
2596
|
+
checkLink(verb, entityType, name, field, value) {
|
|
2597
|
+
if (field.dataType !== "entity" || value === null || value === void 0) return;
|
|
2598
|
+
if (Number.isInteger(value) || typeof value === "string") {
|
|
2599
|
+
const got = typeof value === "number" ? `Integer: ${value}` : `String: ${JSON.stringify(value)}`;
|
|
2600
|
+
throw new SgApiError(400, null, `API ${verb}() ${entityType}.${name} expected [Hash, ActiveSupport::HashWithIndifferentAccess, ActionDispatch::Http::Parameters, ActionDispatch::Http::ParamsHashWithIndifferentAccess, NilClass] data type(s) but got ${got}`);
|
|
2601
|
+
}
|
|
2602
|
+
if (typeof value === "object" && !Array.isArray(value) && typeof value.type !== "string") throw new SgApiError(400, null, `API ${verb}() invalid/missing entity hash string 'type': ${JSON.stringify(value)}`);
|
|
2603
|
+
}
|
|
2604
|
+
/** The next free id of a type, which is what a create takes. */
|
|
2605
|
+
nextId(entityType) {
|
|
2606
|
+
return (this.fixtures.rows.get(entityType) ?? []).reduce((max, row) => Math.max(max, row.id), 0) + 1;
|
|
2607
|
+
}
|
|
2608
|
+
/** The reverse view of a link the server fills in: a Reply lands in `Note.replies`. */
|
|
2609
|
+
linkBack(row) {
|
|
2610
|
+
const push = (owner, field) => {
|
|
2611
|
+
if (!owner) return;
|
|
2612
|
+
const list = this.fixtures.index.get(`${owner.type}:${owner.id}`)?.values[field];
|
|
2613
|
+
if (Array.isArray(list)) list.push(ref(row));
|
|
2614
|
+
};
|
|
2615
|
+
if (row.type === "Reply") push(row.values["entity"], "replies");
|
|
2616
|
+
if (row.type === "Attachment") for (const link of row.values["attachment_links"] ?? []) push(link, "attachments");
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* A Note, its Attachments and its Replies in one list in time order.
|
|
2620
|
+
*
|
|
2621
|
+
* The author key follows the row type, `created_by` on a Note and an Attachment
|
|
2622
|
+
* and `user` on a Reply, whose hash carries an avatar the other two do not, and
|
|
2623
|
+
* so does what `entityFields` can widen: the Reply entry is accepted and changes
|
|
2624
|
+
* nothing (get_entity_notes_id_thread_contents).
|
|
2625
|
+
*/
|
|
2626
|
+
async threadContents(noteId, entityFields) {
|
|
2627
|
+
await this.gate();
|
|
2628
|
+
const note = this.fixtures.index.get(`Note:${noteId}`);
|
|
2629
|
+
if (!note) throw new SgApiError(404, null, `Note: ${noteId} not found`);
|
|
2630
|
+
const linked = (field, type) => (note.values[field] ?? []).map((r) => this.fixtures.index.get(`${type}:${r.id}`)).filter((r) => r !== void 0);
|
|
2631
|
+
const rows = [
|
|
2632
|
+
note,
|
|
2633
|
+
...linked("attachments", "Attachment"),
|
|
2634
|
+
...linked("replies", "Reply")
|
|
2635
|
+
];
|
|
2636
|
+
rows.sort((a, b) => {
|
|
2637
|
+
const at = String(a.values["created_at"] ?? "");
|
|
2638
|
+
const bt = String(b.values["created_at"] ?? "");
|
|
2639
|
+
return at < bt ? -1 : at > bt ? 1 : a.id - b.id;
|
|
2640
|
+
});
|
|
2641
|
+
return rows.map((row) => this.threadRow(row, entityFields?.[row.type] ?? []));
|
|
2642
|
+
}
|
|
2643
|
+
/** One thread row, in the flat shape the endpoint answers. */
|
|
2644
|
+
threadRow(row, widen) {
|
|
2645
|
+
const isReply = row.type === "Reply";
|
|
2646
|
+
const author = this.threadAuthor(row.values[isReply ? "user" : "created_by"], isReply);
|
|
2647
|
+
const fields = {
|
|
2648
|
+
type: row.type,
|
|
2649
|
+
id: row.id,
|
|
2650
|
+
created_at: row.values["created_at"] ?? null
|
|
2651
|
+
};
|
|
2652
|
+
if (row.type !== "Attachment") fields["content"] = row.values["content"] ?? null;
|
|
2653
|
+
fields[isReply ? "user" : "created_by"] = author;
|
|
2654
|
+
if (!isReply) {
|
|
2655
|
+
for (const name of widen) if (SPECS[row.type]?.[name]) fields[name] = row.values[name] ?? null;
|
|
2656
|
+
}
|
|
2657
|
+
return {
|
|
2658
|
+
type: row.type,
|
|
2659
|
+
id: row.id,
|
|
2660
|
+
createdAt: row.values["created_at"] ?? null,
|
|
2661
|
+
content: row.type === "Attachment" ? null : row.values["content"] ?? null,
|
|
2662
|
+
author,
|
|
2663
|
+
fields
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
/** `{id, name, type}`, plus a presigned `image` when the row is a Reply. */
|
|
2667
|
+
threadAuthor(value, withImage) {
|
|
2668
|
+
if (!value) return null;
|
|
2669
|
+
const row = this.fixtures.index.get(`${value.type}:${value.id}`);
|
|
2670
|
+
const author = {
|
|
2671
|
+
type: value.type,
|
|
2672
|
+
id: value.id,
|
|
2673
|
+
name: row ? displayNameOf(row.values, `#${value.id}`) : `#${value.id}`
|
|
2674
|
+
};
|
|
2675
|
+
if (withImage) author.image = row?.values["image"] ?? null;
|
|
2676
|
+
return author;
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* What changed, newest first. `meta` is read off the row because it takes no
|
|
2680
|
+
* filter and no sort, and the cut is made on `project`, `entity`, `event_type`,
|
|
2681
|
+
* `attribute_name` and `created_at` (025_event_log).
|
|
2682
|
+
*/
|
|
2683
|
+
async eventLog(options = {}) {
|
|
2684
|
+
const size = options.page?.size ?? 50;
|
|
2685
|
+
const res = await this.search("EventLogEntry", {
|
|
2686
|
+
filters: eventLogFilters(options),
|
|
2687
|
+
fields: [...EVENT_LOG_FIELDS],
|
|
2688
|
+
sort: "-id",
|
|
2689
|
+
page: {
|
|
2690
|
+
size,
|
|
2691
|
+
number: options.page?.number ?? 1
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
return {
|
|
2695
|
+
data: res.data.map(normalizeEventLogEntry),
|
|
2696
|
+
hasMore: res.hasMore
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Everything one person follows, unpaged. `entity` takes the schema name or the
|
|
2701
|
+
* snake_case plural, and both cuts are made server-side
|
|
2702
|
+
* (get_entity_human_users_id_following).
|
|
2703
|
+
*/
|
|
2704
|
+
async following(userId, options = {}) {
|
|
2705
|
+
await this.gate();
|
|
2706
|
+
if (!this.fixtures.index.get(`HumanUser:${userId}`)) throw new SgApiError(404, null, `Couldn't find HumanUser with id="${userId}"`);
|
|
2707
|
+
if (options.projectId !== void 0 && !this.fixtures.index.get(`Project:${options.projectId}`)) throw new SgApiError(404, null, `Couldn't find Project with id="${options.projectId}"`);
|
|
2708
|
+
let wanted = null;
|
|
2709
|
+
if (options.entity !== void 0) {
|
|
2710
|
+
wanted = entityTypeNamed(options.entity);
|
|
2711
|
+
if (wanted === null) throw new SgApiError(400, { entity: ["entity is not valid"] }, "entity is not valid");
|
|
2712
|
+
}
|
|
2713
|
+
return (this.fixtures.follows.get(userId) ?? []).filter((r) => {
|
|
2714
|
+
if (wanted !== null && r.type !== wanted) return false;
|
|
2715
|
+
if (options.projectId === void 0) return true;
|
|
2716
|
+
return (this.fixtures.index.get(`${r.type}:${r.id}`)?.values["project"])?.id === options.projectId;
|
|
2717
|
+
}).map((r) => ({
|
|
2718
|
+
type: r.type,
|
|
2719
|
+
id: r.id
|
|
2720
|
+
}));
|
|
2721
|
+
}
|
|
2722
|
+
displayNameOfRef(entityRef) {
|
|
2723
|
+
const target = this.fixtures.index.get(`${entityRef.type}:${entityRef.id}`);
|
|
2724
|
+
if (!target) return void 0;
|
|
2725
|
+
return displayNameOf(target.values, `#${entityRef.id}`);
|
|
2726
|
+
}
|
|
2727
|
+
decorate(value) {
|
|
2728
|
+
if (value === null || value === void 0) return null;
|
|
2729
|
+
if (Array.isArray(value)) return value.map((v) => this.decorate(v));
|
|
2730
|
+
const entityRef = value;
|
|
2731
|
+
const name = this.displayNameOfRef(entityRef);
|
|
2732
|
+
return name === void 0 ? {
|
|
2733
|
+
type: entityRef.type,
|
|
2734
|
+
id: entityRef.id
|
|
2735
|
+
} : {
|
|
2736
|
+
type: entityRef.type,
|
|
2737
|
+
id: entityRef.id,
|
|
2738
|
+
name
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
project(row, spec, fields) {
|
|
2742
|
+
const names = fields ?? Object.keys(spec);
|
|
2743
|
+
const attributes = {};
|
|
2744
|
+
const relationships = {};
|
|
2745
|
+
for (const name of names) {
|
|
2746
|
+
if (name.includes(".")) {
|
|
2747
|
+
const resolved = this.resolveProjection(row, spec, name);
|
|
2748
|
+
if (resolved.present) attributes[name] = resolved.value;
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
const field = spec[name];
|
|
2752
|
+
if (!field) continue;
|
|
2753
|
+
if (isLinkType(field.dataType)) relationships[name] = { data: this.decorate(row.values[name]) };
|
|
2754
|
+
else attributes[name] = row.values[name] ?? null;
|
|
2755
|
+
}
|
|
2756
|
+
return {
|
|
2757
|
+
type: row.type,
|
|
2758
|
+
id: row.id,
|
|
2759
|
+
attributes,
|
|
2760
|
+
relationships
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
resolveProjection(row, spec, path) {
|
|
2764
|
+
const segments = path.split(".");
|
|
2765
|
+
const field = spec[segments[0]];
|
|
2766
|
+
if (!field || !isLinkType(field.dataType)) return {
|
|
2767
|
+
present: false,
|
|
2768
|
+
value: null
|
|
2769
|
+
};
|
|
2770
|
+
if (field.dataType === "multi_entity") return {
|
|
2771
|
+
present: false,
|
|
2772
|
+
value: null
|
|
2773
|
+
};
|
|
2774
|
+
const targetType = segments[1];
|
|
2775
|
+
if (!targetType || field.validTypes && !field.validTypes.includes(targetType)) return {
|
|
2776
|
+
present: false,
|
|
2777
|
+
value: null
|
|
2778
|
+
};
|
|
2779
|
+
const values = this.walk(row, path, false);
|
|
2780
|
+
return {
|
|
2781
|
+
present: true,
|
|
2782
|
+
value: values.length > 0 ? values[0] ?? null : null
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Resolve a plain or dotted path to the values it reaches. `strict` makes an
|
|
2787
|
+
* unknown field or an unknown middle type throw the 400 the API answers in a
|
|
2788
|
+
* filter; the projection side wants a silent miss instead.
|
|
2789
|
+
*/
|
|
2790
|
+
walk(row, path, strict) {
|
|
2791
|
+
const segments = path.split(".");
|
|
2792
|
+
let current = [row];
|
|
2793
|
+
let index = 0;
|
|
2794
|
+
while (segments.length - index > 1) {
|
|
2795
|
+
const fieldName = segments[index];
|
|
2796
|
+
const targetType = segments[index + 1];
|
|
2797
|
+
const next = [];
|
|
2798
|
+
for (const node of current) {
|
|
2799
|
+
const field = SPECS[node.type]?.[fieldName];
|
|
2800
|
+
if (!field || !isLinkType(field.dataType)) {
|
|
2801
|
+
if (strict) throw new SgApiError(400, null, `API read() ${node.type}.${fieldName} doesn't exist.`);
|
|
2802
|
+
return [];
|
|
2803
|
+
}
|
|
2804
|
+
if (strict && !SPECS[targetType]) throw new SgApiError(400, null, `API read() ${targetType} is not a valid entity type.`);
|
|
2805
|
+
const raw = node.values[fieldName];
|
|
2806
|
+
const refs = raw === null || raw === void 0 ? [] : Array.isArray(raw) ? raw : [raw];
|
|
2807
|
+
for (const entityRef of refs) {
|
|
2808
|
+
if (entityRef.type !== targetType) continue;
|
|
2809
|
+
const target = this.fixtures.index.get(`${entityRef.type}:${entityRef.id}`);
|
|
2810
|
+
if (target) next.push(target);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
current = next;
|
|
2814
|
+
index += 2;
|
|
2815
|
+
}
|
|
2816
|
+
const leaf = segments[index];
|
|
2817
|
+
const out = [];
|
|
2818
|
+
for (const node of current) {
|
|
2819
|
+
if (!SPECS[node.type]?.[leaf]) {
|
|
2820
|
+
if (strict) throw new SgApiError(400, null, `API read() ${node.type}.${leaf} doesn't exist.`);
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
out.push(node.values[leaf] ?? null);
|
|
2824
|
+
}
|
|
2825
|
+
return out;
|
|
2826
|
+
}
|
|
2827
|
+
/** The data type a filter path lands on, for choosing the comparison and validating the operator. */
|
|
2828
|
+
dataTypeAt(entityType, path) {
|
|
2829
|
+
const segments = path.split(".");
|
|
2830
|
+
let type = entityType;
|
|
2831
|
+
let index = 0;
|
|
2832
|
+
while (segments.length - index > 1) {
|
|
2833
|
+
const fieldName = segments[index];
|
|
2834
|
+
if (!SPECS[type]?.[fieldName]) throw new SgApiError(400, null, `API read() ${type}.${fieldName} doesn't exist.`);
|
|
2835
|
+
type = segments[index + 1];
|
|
2836
|
+
if (!SPECS[type]) throw new SgApiError(400, null, `API read() ${type} is not a valid entity type.`);
|
|
2837
|
+
index += 2;
|
|
2838
|
+
}
|
|
2839
|
+
const leaf = segments[index];
|
|
2840
|
+
const field = SPECS[type]?.[leaf];
|
|
2841
|
+
if (!field) throw new SgApiError(400, null, `API read() ${type}.${leaf} doesn't exist.`);
|
|
2842
|
+
return field.dataType;
|
|
2843
|
+
}
|
|
2844
|
+
linkedPair(row) {
|
|
2845
|
+
for (const fieldName of [
|
|
2846
|
+
"entity",
|
|
2847
|
+
"sg_sequence",
|
|
2848
|
+
"project"
|
|
2849
|
+
]) {
|
|
2850
|
+
const value = row.values[fieldName];
|
|
2851
|
+
if (value && !Array.isArray(value)) {
|
|
2852
|
+
const entityRef = value;
|
|
2853
|
+
const name = this.displayNameOfRef(entityRef);
|
|
2854
|
+
if (name !== void 0) return [entityRef.type, name];
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return ["", ""];
|
|
2858
|
+
}
|
|
2859
|
+
matchGroup(row, entityType, group) {
|
|
2860
|
+
if (!group) return true;
|
|
2861
|
+
const results = group.conditions.map((c) => Array.isArray(c) ? this.matchCondition(row, entityType, c) : this.matchGroup(row, entityType, c));
|
|
2862
|
+
if (results.length === 0) return true;
|
|
2863
|
+
return group.logical_operator === "or" ? results.some(Boolean) : results.every(Boolean);
|
|
2864
|
+
}
|
|
2865
|
+
matchCondition(row, entityType, [path, operator, expected]) {
|
|
2866
|
+
const dataType = this.dataTypeAt(entityType, path);
|
|
2867
|
+
if (!isFilterable(dataType)) throw new SgApiError(400, null, `API read() ${entityType}.${path}'s '${dataType}' data type cannot be used in a filter.`);
|
|
2868
|
+
if (!operatorsFor(dataType).includes(operator)) throw new SgApiError(400, null, `API read() ${entityType}.${path}'s '${dataType}' data type doesn't support '${operator}' 'relation'`);
|
|
2869
|
+
const raw = this.walk(row, path, true);
|
|
2870
|
+
const values = operator.startsWith("name_") ? raw.map((v) => this.decorate(v)) : raw;
|
|
2871
|
+
const now = this.clock();
|
|
2872
|
+
if (values.length > 1) return values.some((v) => evaluate(dataType, operator, v ?? null, expected, now));
|
|
2873
|
+
return evaluate(dataType, operator, values.length === 0 ? null : values[0] ?? null, expected, now);
|
|
2874
|
+
}
|
|
2875
|
+
applySort(rows, entityType, sort) {
|
|
2876
|
+
const out = [...rows].sort((a, b) => a.id - b.id);
|
|
2877
|
+
if (!sort) return out;
|
|
2878
|
+
const keys = sort.split(",").map((k) => k.trim()).filter(Boolean).map((k) => k.startsWith("-") ? {
|
|
2879
|
+
field: k.slice(1),
|
|
2880
|
+
descending: true
|
|
2881
|
+
} : {
|
|
2882
|
+
field: k,
|
|
2883
|
+
descending: false
|
|
2884
|
+
}).filter((k) => Boolean(SPECS[entityType]?.[k.field]) || k.field.includes("."));
|
|
2885
|
+
if (keys.length === 0) return out;
|
|
2886
|
+
return out.sort((a, b) => {
|
|
2887
|
+
for (const key of keys) {
|
|
2888
|
+
const c = sortCompare(this.walk(a, key.field, false)[0] ?? null, this.walk(b, key.field, false)[0] ?? null);
|
|
2889
|
+
if (c !== 0) return key.descending ? -c : c;
|
|
2890
|
+
}
|
|
2891
|
+
return a.id - b.id;
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
};
|
|
2895
|
+
/** A type named as a schema name or as its snake_case plural, or null for neither. */
|
|
2896
|
+
function entityTypeNamed(name) {
|
|
2897
|
+
if (SPECS[name]) return name;
|
|
2898
|
+
const wanted = name.toLowerCase();
|
|
2899
|
+
return Object.keys(SPECS).find((type) => type.toLowerCase() === wanted || pluralPath(type) === wanted) ?? null;
|
|
2900
|
+
}
|
|
2901
|
+
function isNullish(v) {
|
|
2902
|
+
return v === null || v === void 0 || v === "";
|
|
2903
|
+
}
|
|
2904
|
+
function refsOf(value) {
|
|
2905
|
+
if (value === null || value === void 0) return [];
|
|
2906
|
+
return Array.isArray(value) ? value : [value];
|
|
2907
|
+
}
|
|
2908
|
+
function sameScalar(dataType, actual, expected) {
|
|
2909
|
+
if (isNullish(expected)) return isNullish(actual);
|
|
2910
|
+
if (isNullish(actual)) return false;
|
|
2911
|
+
if (isLinkType(dataType)) {
|
|
2912
|
+
const wanted = expected;
|
|
2913
|
+
if (typeof wanted !== "object" || wanted === null) return false;
|
|
2914
|
+
return refsOf(actual).some((r) => r.type === wanted.type && r.id === wanted.id);
|
|
2915
|
+
}
|
|
2916
|
+
if (isNumericType(dataType)) {
|
|
2917
|
+
const a = Number(actual);
|
|
2918
|
+
const b = Number(expected);
|
|
2919
|
+
return !Number.isNaN(a) && !Number.isNaN(b) && a === b;
|
|
2920
|
+
}
|
|
2921
|
+
if (typeof actual === "string" && typeof expected === "string") return actual.toLowerCase() === expected.toLowerCase();
|
|
2922
|
+
return actual === expected;
|
|
2923
|
+
}
|
|
2924
|
+
function substring(actual, expected, mode) {
|
|
2925
|
+
if (isNullish(actual) || expected === null || expected === void 0) return false;
|
|
2926
|
+
const a = String(actual).toLowerCase();
|
|
2927
|
+
const b = String(expected).toLowerCase();
|
|
2928
|
+
return mode === "contains" ? a.includes(b) : mode === "starts" ? a.startsWith(b) : a.endsWith(b);
|
|
2929
|
+
}
|
|
2930
|
+
/** Order two filter values. Returns null when either is unset, which is what excludes null rows. */
|
|
2931
|
+
function compare(a, b) {
|
|
2932
|
+
if (isNullish(a) || isNullish(b)) return null;
|
|
2933
|
+
if (typeof a === "number" || typeof b === "number") {
|
|
2934
|
+
const x = Number(a);
|
|
2935
|
+
const y = Number(b);
|
|
2936
|
+
return Number.isNaN(x) || Number.isNaN(y) ? null : x - y;
|
|
2937
|
+
}
|
|
2938
|
+
const x = String(a);
|
|
2939
|
+
const y = String(b);
|
|
2940
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
2941
|
+
}
|
|
2942
|
+
/** Sort order, with nulls last in both directions (entity_types/Step, on `list_order`). */
|
|
2943
|
+
function sortCompare(a, b) {
|
|
2944
|
+
const aNull = isNullish(a);
|
|
2945
|
+
const bNull = isNullish(b);
|
|
2946
|
+
if (aNull && bNull) return 0;
|
|
2947
|
+
if (aNull) return 1;
|
|
2948
|
+
if (bNull) return -1;
|
|
2949
|
+
return compare(a, b) ?? 0;
|
|
2950
|
+
}
|
|
2951
|
+
const HOUR_MS = 36e5;
|
|
2952
|
+
const DAY_MS = 864e5;
|
|
2953
|
+
function toClock(now) {
|
|
2954
|
+
if (typeof now === "function") return now;
|
|
2955
|
+
if (typeof now === "number") return () => now;
|
|
2956
|
+
if (typeof now === "string") {
|
|
2957
|
+
const fixed = Date.parse(now);
|
|
2958
|
+
if (Number.isNaN(fixed)) throw new TypeError(`MockClient: 'now' is not a date: ${now}`);
|
|
2959
|
+
return () => fixed;
|
|
2960
|
+
}
|
|
2961
|
+
return () => Date.now();
|
|
2962
|
+
}
|
|
2963
|
+
const RELATIVE_OPERATORS = /* @__PURE__ */ new Set([
|
|
2964
|
+
"in_last",
|
|
2965
|
+
"not_in_last",
|
|
2966
|
+
"in_next",
|
|
2967
|
+
"not_in_next"
|
|
2968
|
+
]);
|
|
2969
|
+
const CALENDAR_OPERATORS = /* @__PURE__ */ new Set([
|
|
2970
|
+
"in_calendar_day",
|
|
2971
|
+
"in_calendar_week",
|
|
2972
|
+
"in_calendar_month",
|
|
2973
|
+
"in_calendar_year"
|
|
2974
|
+
]);
|
|
2975
|
+
/** A value list the way the API prints one back in an error. */
|
|
2976
|
+
function printList(values) {
|
|
2977
|
+
return `[${values.map((v) => JSON.stringify(v)).join(", ")}]`;
|
|
2978
|
+
}
|
|
2979
|
+
/** `[count, UNIT]`, with the three 400s the API answers for a malformed one (field_types/date). */
|
|
2980
|
+
function relativeValue(operator, expected) {
|
|
2981
|
+
const parts = Array.isArray(expected) ? expected : [expected];
|
|
2982
|
+
if (parts.length !== 2) throw new SgApiError(400, null, `API read() '${operator}' 'relation' expects a 2-element array: ${printList(parts)}`);
|
|
2983
|
+
const [count, unit] = parts;
|
|
2984
|
+
if (!TIME_UNITS.includes(unit)) throw new SgApiError(400, null, `API read() '${operator}' 'relation' doesn't support the '${String(unit)}' time unit: ${printList(parts)} Valid time units: ${printList(TIME_UNITS)}`);
|
|
2985
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count <= 0) throw new SgApiError(400, null, `API read() '${operator}' 'relation' expects at a positive Integer time unit`);
|
|
2986
|
+
return [count, unit];
|
|
2987
|
+
}
|
|
2988
|
+
/** Shift a UTC instant by whole months, clamping a day the target month does not have. */
|
|
2989
|
+
function shiftMonths(t, months) {
|
|
2990
|
+
const d = new Date(t);
|
|
2991
|
+
const day = d.getUTCDate();
|
|
2992
|
+
const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months + 1, 0)).getUTCDate();
|
|
2993
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, Math.min(day, last), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
2994
|
+
}
|
|
2995
|
+
/** The window `[count, UNIT]` names: back to `now`, or forward from it. */
|
|
2996
|
+
function relativeWindow(now, count, unit, forward) {
|
|
2997
|
+
const sign = forward ? 1 : -1;
|
|
2998
|
+
const edge = unit === "HOUR" ? now + sign * count * HOUR_MS : unit === "DAY" ? now + sign * count * DAY_MS : unit === "WEEK" ? now + sign * count * 7 * DAY_MS : unit === "MONTH" ? shiftMonths(now, sign * count) : shiftMonths(now, sign * count * 12);
|
|
2999
|
+
return forward ? [now, edge] : [edge, now];
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* The UTC calendar bucket `offset` from the one holding `now`, inclusive at both ends.
|
|
3003
|
+
* A week runs Monday to Sunday: the corpus pins the buckets to UTC but not the first day.
|
|
3004
|
+
*/
|
|
3005
|
+
function calendarWindow(now, operator, offset) {
|
|
3006
|
+
const d = new Date(now);
|
|
3007
|
+
const year = d.getUTCFullYear();
|
|
3008
|
+
const month = d.getUTCMonth();
|
|
3009
|
+
if (operator === "in_calendar_year") return [Date.UTC(year + offset, 0, 1), Date.UTC(year + offset + 1, 0, 1) - 1];
|
|
3010
|
+
if (operator === "in_calendar_month") return [Date.UTC(year, month + offset, 1), Date.UTC(year, month + offset + 1, 1) - 1];
|
|
3011
|
+
const midnight = Date.UTC(year, month, d.getUTCDate());
|
|
3012
|
+
if (operator === "in_calendar_day") {
|
|
3013
|
+
const start = midnight + offset * DAY_MS;
|
|
3014
|
+
return [start, start + DAY_MS - 1];
|
|
3015
|
+
}
|
|
3016
|
+
const start = midnight - (d.getUTCDay() + 6) % 7 * DAY_MS + offset * 7 * DAY_MS;
|
|
3017
|
+
return [start, start + 7 * DAY_MS - 1];
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* The inclusive span a stored value covers. A `date` has no time of day and stands for its whole
|
|
3021
|
+
* UTC day, which is why a window shorter than a day still matches today (field_types/date).
|
|
3022
|
+
*/
|
|
3023
|
+
function span(dataType, value) {
|
|
3024
|
+
if (typeof value !== "string") return null;
|
|
3025
|
+
const t = Date.parse(value);
|
|
3026
|
+
if (Number.isNaN(t)) return null;
|
|
3027
|
+
return dataType === "date" ? [t, t + DAY_MS - 1] : [t, t];
|
|
3028
|
+
}
|
|
3029
|
+
function within(dataType, actual, window) {
|
|
3030
|
+
const covered = span(dataType, actual);
|
|
3031
|
+
return covered !== null && covered[0] <= window[1] && covered[1] >= window[0];
|
|
3032
|
+
}
|
|
3033
|
+
/** A relative or calendar date operator, resolved against the clock. */
|
|
3034
|
+
function matchTemporal(dataType, operator, actual, expected, now) {
|
|
3035
|
+
if (RELATIVE_OPERATORS.has(operator)) {
|
|
3036
|
+
const [count, unit] = relativeValue(operator, expected);
|
|
3037
|
+
const negating = NEGATING_OPERATORS.has(operator);
|
|
3038
|
+
if (isNullish(actual)) return negating;
|
|
3039
|
+
const hit = within(dataType, actual, relativeWindow(now, count, unit, operator === "in_next" || operator === "not_in_next"));
|
|
3040
|
+
return negating ? !hit : hit;
|
|
3041
|
+
}
|
|
3042
|
+
const offset = Number(Array.isArray(expected) ? expected[0] : expected);
|
|
3043
|
+
if (!Number.isInteger(offset)) return false;
|
|
3044
|
+
return within(dataType, actual, calendarWindow(now, operator, offset));
|
|
3045
|
+
}
|
|
3046
|
+
function namesOf(actual) {
|
|
3047
|
+
return refsOf(actual).map((r) => typeof r.name === "string" ? r.name : "").filter(Boolean);
|
|
3048
|
+
}
|
|
3049
|
+
function evaluate(dataType, operator, actual, expected, now) {
|
|
3050
|
+
if (RELATIVE_OPERATORS.has(operator) || CALENDAR_OPERATORS.has(operator)) return matchTemporal(dataType, operator, actual, expected, now);
|
|
3051
|
+
if (NEGATING_OPERATORS.has(operator) && isNullish(actual)) return true;
|
|
3052
|
+
switch (operator) {
|
|
3053
|
+
case "is": return sameScalar(dataType, actual, expected);
|
|
3054
|
+
case "is_not": return !sameScalar(dataType, actual, expected);
|
|
3055
|
+
case "in": return asList(expected).some((e) => sameScalar(dataType, actual, e));
|
|
3056
|
+
case "not_in": return !asList(expected).some((e) => sameScalar(dataType, actual, e));
|
|
3057
|
+
case "contains": return substring(actual, expected, "contains");
|
|
3058
|
+
case "not_contains": return !substring(actual, expected, "contains");
|
|
3059
|
+
case "starts_with": return substring(actual, expected, "starts");
|
|
3060
|
+
case "ends_with": return substring(actual, expected, "ends");
|
|
3061
|
+
case "greater_than": {
|
|
3062
|
+
const c = compare(actual, expected);
|
|
3063
|
+
return c !== null && c > 0;
|
|
3064
|
+
}
|
|
3065
|
+
case "less_than": {
|
|
3066
|
+
const c = compare(actual, expected);
|
|
3067
|
+
return c !== null && c < 0;
|
|
3068
|
+
}
|
|
3069
|
+
case "between": {
|
|
3070
|
+
const [lo, hi] = asList(expected);
|
|
3071
|
+
const a = compare(actual, lo);
|
|
3072
|
+
const b = compare(actual, hi);
|
|
3073
|
+
if (a === null || b === null) return false;
|
|
3074
|
+
return a >= 0 && b <= 0 || a <= 0 && b >= 0;
|
|
3075
|
+
}
|
|
3076
|
+
case "type_is": return refsOf(actual).some((r) => r.type === expected);
|
|
3077
|
+
case "type_is_not": return !refsOf(actual).some((r) => r.type === expected);
|
|
3078
|
+
case "name_is": return namesOf(actual).some((n) => n.toLowerCase() === String(expected).toLowerCase());
|
|
3079
|
+
case "name_contains": return namesOf(actual).some((n) => n.toLowerCase().includes(String(expected).toLowerCase()));
|
|
3080
|
+
case "name_not_contains": return !namesOf(actual).some((n) => n.toLowerCase().includes(String(expected).toLowerCase()));
|
|
3081
|
+
default: return false;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
function asList(value) {
|
|
3085
|
+
return Array.isArray(value) ? value : [value];
|
|
3086
|
+
}
|
|
3087
|
+
/** On an entity grouping the label is the target's display name, not the whole hash (020_summarize). */
|
|
3088
|
+
function groupLabel(value) {
|
|
3089
|
+
if (value !== null && typeof value === "object" && "type" in value) {
|
|
3090
|
+
const ref = value;
|
|
3091
|
+
return ref.name ?? `${ref.type} #${ref.id}`;
|
|
3092
|
+
}
|
|
3093
|
+
return String(value);
|
|
3094
|
+
}
|
|
3095
|
+
function toStatusIcon(values) {
|
|
3096
|
+
switch (values["display_type"]) {
|
|
3097
|
+
case "image_map": return {
|
|
3098
|
+
displayType: "image_map",
|
|
3099
|
+
imageMapKey: String(values["image_map_key"] ?? "")
|
|
3100
|
+
};
|
|
3101
|
+
case "image": return {
|
|
3102
|
+
displayType: "image",
|
|
3103
|
+
dataUrl: String(values["url"] ?? "").replace(/\s+/g, "")
|
|
3104
|
+
};
|
|
3105
|
+
case "html": return {
|
|
3106
|
+
displayType: "html",
|
|
3107
|
+
html: String(values["html"] ?? "")
|
|
3108
|
+
};
|
|
3109
|
+
default: return null;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
//#endregion
|
|
3113
|
+
export { MOCK_NOW, MockClient };
|