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/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 };