sg-widgets-core 0.2.0 → 0.3.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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as NEGATING_OPERATORS, D as toFilterArray, I as isFilterable, L as isLinkType, N as TIME_UNITS, R as isNumericType, V as operatorsFor, a as eventLogFilters, d as displayNameOf, f as fieldSchemaOverride, r as SgApiError, s as normalizeEventLogEntry, t as EVENT_LOG_FIELDS, v as pluralPath } from "./client-
|
|
1
|
+
import { A as NEGATING_OPERATORS, D as toFilterArray, I as isFilterable, L as isLinkType, N as TIME_UNITS, R as isNumericType, V as operatorsFor, a as eventLogFilters, d as displayNameOf, f as fieldSchemaOverride, r as SgApiError, s as normalizeEventLogEntry, t as EVENT_LOG_FIELDS, v as pluralPath } from "./client-izKszX7l.mjs";
|
|
2
2
|
//#region src/mock.ts
|
|
3
3
|
/**
|
|
4
4
|
* Status vocabularies. `valid_values` is the site's whole vocabulary and is
|
|
@@ -151,6 +151,19 @@ const PROJECT_STATUSES = [
|
|
|
151
151
|
"Complete",
|
|
152
152
|
"On Hold"
|
|
153
153
|
];
|
|
154
|
+
/** PublishedFile's list is site configuration; this site keeps the three the probed one held (entity_types/PublishedFile). */
|
|
155
|
+
const PUBLISHED_FILE_STATUSES = [
|
|
156
|
+
"wtg",
|
|
157
|
+
"ip",
|
|
158
|
+
"cmpt"
|
|
159
|
+
];
|
|
160
|
+
/** The four `TaskDependency.dependency_type` values, the first the default (085_task_dependency_types). */
|
|
161
|
+
const DEPENDENCY_TYPES = [
|
|
162
|
+
"finish-to-start-next-day",
|
|
163
|
+
"start-to-start",
|
|
164
|
+
"finish-to-finish",
|
|
165
|
+
"start-to-finish-next-day"
|
|
166
|
+
];
|
|
154
167
|
const AUDIT = {
|
|
155
168
|
id: {
|
|
156
169
|
displayName: "Id",
|
|
@@ -277,6 +290,15 @@ const SPECS = {
|
|
|
277
290
|
displayName: "Users",
|
|
278
291
|
dataType: "multi_entity",
|
|
279
292
|
validTypes: ["HumanUser"]
|
|
293
|
+
},
|
|
294
|
+
tracking_settings: {
|
|
295
|
+
displayName: "Tracking Settings",
|
|
296
|
+
dataType: "serializable"
|
|
297
|
+
},
|
|
298
|
+
task_templates: {
|
|
299
|
+
displayName: "Task Templates",
|
|
300
|
+
dataType: "multi_entity",
|
|
301
|
+
validTypes: ["TaskTemplate"]
|
|
280
302
|
}
|
|
281
303
|
},
|
|
282
304
|
Sequence: {
|
|
@@ -313,6 +335,11 @@ const SPECS = {
|
|
|
313
335
|
displayName: "Shots",
|
|
314
336
|
dataType: "multi_entity",
|
|
315
337
|
validTypes: ["Shot"]
|
|
338
|
+
},
|
|
339
|
+
task_template: {
|
|
340
|
+
displayName: "Task Template",
|
|
341
|
+
dataType: "entity",
|
|
342
|
+
validTypes: ["TaskTemplate"]
|
|
316
343
|
}
|
|
317
344
|
},
|
|
318
345
|
Shot: {
|
|
@@ -392,6 +419,11 @@ const SPECS = {
|
|
|
392
419
|
displayName: "Tasks",
|
|
393
420
|
dataType: "multi_entity",
|
|
394
421
|
validTypes: ["Task"]
|
|
422
|
+
},
|
|
423
|
+
task_template: {
|
|
424
|
+
displayName: "Task Template",
|
|
425
|
+
dataType: "entity",
|
|
426
|
+
validTypes: ["TaskTemplate"]
|
|
395
427
|
}
|
|
396
428
|
},
|
|
397
429
|
Asset: {
|
|
@@ -451,6 +483,11 @@ const SPECS = {
|
|
|
451
483
|
displayName: "Tasks",
|
|
452
484
|
dataType: "multi_entity",
|
|
453
485
|
validTypes: ["Task"]
|
|
486
|
+
},
|
|
487
|
+
task_template: {
|
|
488
|
+
displayName: "Task Template",
|
|
489
|
+
dataType: "entity",
|
|
490
|
+
validTypes: ["TaskTemplate"]
|
|
454
491
|
}
|
|
455
492
|
},
|
|
456
493
|
Version: {
|
|
@@ -634,6 +671,151 @@ const SPECS = {
|
|
|
634
671
|
displayName: "Upstream Tasks",
|
|
635
672
|
dataType: "multi_entity",
|
|
636
673
|
validTypes: ["Task"]
|
|
674
|
+
},
|
|
675
|
+
downstream_tasks: {
|
|
676
|
+
displayName: "Downstream Tasks",
|
|
677
|
+
dataType: "multi_entity",
|
|
678
|
+
validTypes: ["Task"]
|
|
679
|
+
},
|
|
680
|
+
sg_sort_order: {
|
|
681
|
+
displayName: "Sort Order",
|
|
682
|
+
dataType: "number"
|
|
683
|
+
},
|
|
684
|
+
task_template: {
|
|
685
|
+
displayName: "Task Template",
|
|
686
|
+
dataType: "entity",
|
|
687
|
+
validTypes: ["TaskTemplate"]
|
|
688
|
+
},
|
|
689
|
+
template_task: {
|
|
690
|
+
displayName: "Template Task",
|
|
691
|
+
dataType: "entity",
|
|
692
|
+
validTypes: ["Task"]
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
TaskTemplate: {
|
|
696
|
+
...AUDIT,
|
|
697
|
+
code: {
|
|
698
|
+
displayName: "Template Name",
|
|
699
|
+
dataType: "text",
|
|
700
|
+
mandatory: true
|
|
701
|
+
},
|
|
702
|
+
description: {
|
|
703
|
+
displayName: "Description",
|
|
704
|
+
dataType: "text"
|
|
705
|
+
},
|
|
706
|
+
entity_type: {
|
|
707
|
+
displayName: "Entity Type",
|
|
708
|
+
dataType: "entity_type",
|
|
709
|
+
editable: false,
|
|
710
|
+
writable: true
|
|
711
|
+
},
|
|
712
|
+
projects: {
|
|
713
|
+
displayName: "Projects",
|
|
714
|
+
dataType: "multi_entity",
|
|
715
|
+
validTypes: ["Project"]
|
|
716
|
+
},
|
|
717
|
+
task_count: {
|
|
718
|
+
displayName: "Task Count",
|
|
719
|
+
dataType: "number",
|
|
720
|
+
editable: false
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
TaskDependency: {
|
|
724
|
+
...AUDIT,
|
|
725
|
+
task: {
|
|
726
|
+
displayName: "Task",
|
|
727
|
+
dataType: "entity",
|
|
728
|
+
validTypes: ["Task"]
|
|
729
|
+
},
|
|
730
|
+
dependent_task: {
|
|
731
|
+
displayName: "Dependent Task",
|
|
732
|
+
dataType: "entity",
|
|
733
|
+
validTypes: ["Task"]
|
|
734
|
+
},
|
|
735
|
+
dependency_type: {
|
|
736
|
+
displayName: "Dependency Type",
|
|
737
|
+
dataType: "text",
|
|
738
|
+
defaultValue: DEPENDENCY_TYPES[0]
|
|
739
|
+
},
|
|
740
|
+
offset_days: {
|
|
741
|
+
displayName: "Offset Days",
|
|
742
|
+
dataType: "number"
|
|
743
|
+
},
|
|
744
|
+
shift_ratio: {
|
|
745
|
+
displayName: "Shift Ratio",
|
|
746
|
+
dataType: "float"
|
|
747
|
+
},
|
|
748
|
+
task_id: {
|
|
749
|
+
displayName: "Task Id",
|
|
750
|
+
dataType: "number",
|
|
751
|
+
editable: false
|
|
752
|
+
},
|
|
753
|
+
dependent_task_id: {
|
|
754
|
+
displayName: "Dependent Task Id",
|
|
755
|
+
dataType: "number",
|
|
756
|
+
editable: false
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
PublishedFile: {
|
|
760
|
+
...AUDIT,
|
|
761
|
+
code: {
|
|
762
|
+
displayName: "Published File Name",
|
|
763
|
+
dataType: "text",
|
|
764
|
+
mandatory: true
|
|
765
|
+
},
|
|
766
|
+
name: {
|
|
767
|
+
displayName: "Name",
|
|
768
|
+
dataType: "text"
|
|
769
|
+
},
|
|
770
|
+
description: {
|
|
771
|
+
displayName: "Description",
|
|
772
|
+
dataType: "text"
|
|
773
|
+
},
|
|
774
|
+
version_number: {
|
|
775
|
+
displayName: "Version Number",
|
|
776
|
+
dataType: "number"
|
|
777
|
+
},
|
|
778
|
+
path: {
|
|
779
|
+
displayName: "Path",
|
|
780
|
+
dataType: "url"
|
|
781
|
+
},
|
|
782
|
+
path_cache: {
|
|
783
|
+
displayName: "Path Cache",
|
|
784
|
+
dataType: "text"
|
|
785
|
+
},
|
|
786
|
+
sg_status_list: statusSpec(PUBLISHED_FILE_STATUSES, "wtg"),
|
|
787
|
+
image: {
|
|
788
|
+
displayName: "Thumbnail",
|
|
789
|
+
dataType: "image"
|
|
790
|
+
},
|
|
791
|
+
project: {
|
|
792
|
+
displayName: "Project",
|
|
793
|
+
dataType: "entity",
|
|
794
|
+
validTypes: ["Project"]
|
|
795
|
+
},
|
|
796
|
+
entity: {
|
|
797
|
+
displayName: "Link",
|
|
798
|
+
dataType: "entity",
|
|
799
|
+
validTypes: [
|
|
800
|
+
"Asset",
|
|
801
|
+
"Shot",
|
|
802
|
+
"Sequence"
|
|
803
|
+
]
|
|
804
|
+
},
|
|
805
|
+
task: {
|
|
806
|
+
displayName: "Task",
|
|
807
|
+
dataType: "entity",
|
|
808
|
+
validTypes: ["Task"]
|
|
809
|
+
},
|
|
810
|
+
version: {
|
|
811
|
+
displayName: "Version",
|
|
812
|
+
dataType: "entity",
|
|
813
|
+
validTypes: ["Version"]
|
|
814
|
+
},
|
|
815
|
+
published_file_type: {
|
|
816
|
+
displayName: "Published File Type",
|
|
817
|
+
dataType: "entity",
|
|
818
|
+
validTypes: ["PublishedFileType"]
|
|
637
819
|
}
|
|
638
820
|
},
|
|
639
821
|
Note: {
|
|
@@ -1048,15 +1230,26 @@ const IDENTITY_FIELD = {
|
|
|
1048
1230
|
Task: "content",
|
|
1049
1231
|
Note: "subject",
|
|
1050
1232
|
Reply: "content",
|
|
1051
|
-
Attachment: "display_name"
|
|
1233
|
+
Attachment: "display_name",
|
|
1234
|
+
TaskTemplate: "code",
|
|
1235
|
+
PublishedFile: "code"
|
|
1052
1236
|
};
|
|
1053
1237
|
const GENERATED_IDENTITY = /* @__PURE__ */ new Set([
|
|
1054
1238
|
"Sequence",
|
|
1055
1239
|
"Shot",
|
|
1056
1240
|
"Asset",
|
|
1057
1241
|
"Version",
|
|
1058
|
-
"Task"
|
|
1242
|
+
"Task",
|
|
1243
|
+
"TaskTemplate",
|
|
1244
|
+
"PublishedFile"
|
|
1059
1245
|
]);
|
|
1246
|
+
/** The name a generated identity starts with, where it is not the display name (entity_types/TaskTemplate). */
|
|
1247
|
+
const GENERATED_NAME = { TaskTemplate: "TaskTemplate" };
|
|
1248
|
+
/** Where one side of a link is written, the other side follows (088_project_template_defaults). */
|
|
1249
|
+
const MIRRORED = {
|
|
1250
|
+
"Project.task_templates": ["TaskTemplate", "projects"],
|
|
1251
|
+
"TaskTemplate.projects": ["Project", "task_templates"]
|
|
1252
|
+
};
|
|
1060
1253
|
const DISPLAY_NAMES = {
|
|
1061
1254
|
Project: "Project",
|
|
1062
1255
|
Sequence: "Sequence",
|
|
@@ -1072,13 +1265,31 @@ const DISPLAY_NAMES = {
|
|
|
1072
1265
|
Note: "Note",
|
|
1073
1266
|
Reply: "Reply",
|
|
1074
1267
|
Attachment: "Attachment",
|
|
1075
|
-
EventLogEntry: "Event Log Entry"
|
|
1268
|
+
EventLogEntry: "Event Log Entry",
|
|
1269
|
+
TaskTemplate: "Task Template",
|
|
1270
|
+
TaskDependency: "Task Dependency",
|
|
1271
|
+
PublishedFile: "Published File"
|
|
1076
1272
|
};
|
|
1077
1273
|
/**
|
|
1078
1274
|
* Which codes each project hides, per type. `hidden_values` is the only thing
|
|
1079
1275
|
* `project_id` changes (009_status_lists), and it is not a subset of
|
|
1080
1276
|
* `valid_values`: the probed site hid `blk` and `rdy`, neither of them valid.
|
|
1081
1277
|
*/
|
|
1278
|
+
/**
|
|
1279
|
+
* The fields this site may hide, `visible.editable` true: its own additions and a few
|
|
1280
|
+
* stock fields, `Project.code` among them. Every other field reads false, `sg_` prefix
|
|
1281
|
+
* or not (056_stock_vs_custom_field).
|
|
1282
|
+
*/
|
|
1283
|
+
const HIDEABLE = /* @__PURE__ */ new Set([
|
|
1284
|
+
"Project.code",
|
|
1285
|
+
"Shot.sg_complexity",
|
|
1286
|
+
"Shot.sg_lens",
|
|
1287
|
+
"Shot.sg_shot_notes_url",
|
|
1288
|
+
"Asset.sg_build_days",
|
|
1289
|
+
"Asset.sg_complexity",
|
|
1290
|
+
"Version.sg_department",
|
|
1291
|
+
"Version.sg_bar_color"
|
|
1292
|
+
]);
|
|
1082
1293
|
const HIDDEN_VALUES = {
|
|
1083
1294
|
70: {
|
|
1084
1295
|
"Version.sg_status_list": [
|
|
@@ -1980,6 +2191,124 @@ function buildFixtures(seed, counts = {}) {
|
|
|
1980
2191
|
});
|
|
1981
2192
|
eventId += eventId % 7 === 0 ? 3 : 1;
|
|
1982
2193
|
}
|
|
2194
|
+
let dependencyId = 1;
|
|
2195
|
+
const depend = (downstream, upstream) => {
|
|
2196
|
+
add("TaskDependency", dependencyId, {
|
|
2197
|
+
cached_display_name: `Task ${downstream.id} dependent on Task ${upstream.id}`,
|
|
2198
|
+
task: ref(downstream),
|
|
2199
|
+
dependent_task: ref(upstream),
|
|
2200
|
+
dependency_type: DEPENDENCY_TYPES[0],
|
|
2201
|
+
offset_days: null,
|
|
2202
|
+
shift_ratio: null,
|
|
2203
|
+
task_id: downstream.id,
|
|
2204
|
+
dependent_task_id: upstream.id,
|
|
2205
|
+
created_at: isoDateTime(-180),
|
|
2206
|
+
updated_at: isoDateTime(-180),
|
|
2207
|
+
created_by: ref(bot),
|
|
2208
|
+
updated_by: ref(bot)
|
|
2209
|
+
});
|
|
2210
|
+
dependencyId += 1;
|
|
2211
|
+
};
|
|
2212
|
+
let templateTaskId = 5600;
|
|
2213
|
+
const [shotTemplate, assetTemplate] = [{
|
|
2214
|
+
id: 40,
|
|
2215
|
+
code: "Shot basic",
|
|
2216
|
+
entityType: "Shot"
|
|
2217
|
+
}, {
|
|
2218
|
+
id: 41,
|
|
2219
|
+
code: "Asset build",
|
|
2220
|
+
entityType: "Asset"
|
|
2221
|
+
}].map((t) => {
|
|
2222
|
+
const template = add("TaskTemplate", t.id, {
|
|
2223
|
+
code: t.code,
|
|
2224
|
+
cached_display_name: t.code,
|
|
2225
|
+
description: `Every ${t.entityType} step, one after the other.`,
|
|
2226
|
+
entity_type: t.entityType,
|
|
2227
|
+
projects: [],
|
|
2228
|
+
task_count: null,
|
|
2229
|
+
created_at: isoDateTime(-190),
|
|
2230
|
+
updated_at: isoDateTime(-190),
|
|
2231
|
+
created_by: ref(bot),
|
|
2232
|
+
updated_by: ref(bot)
|
|
2233
|
+
});
|
|
2234
|
+
let previous = null;
|
|
2235
|
+
steps.filter((st) => st.values["entity_type"] === t.entityType).forEach((st, i) => {
|
|
2236
|
+
const content = String(st.values["code"]);
|
|
2237
|
+
const templateTask = add("Task", templateTaskId, {
|
|
2238
|
+
content,
|
|
2239
|
+
cached_display_name: content,
|
|
2240
|
+
sg_description: null,
|
|
2241
|
+
sg_status_list: "wtg",
|
|
2242
|
+
start_date: null,
|
|
2243
|
+
due_date: null,
|
|
2244
|
+
duration: null,
|
|
2245
|
+
est_in_mins: null,
|
|
2246
|
+
time_logs_sum: 0,
|
|
2247
|
+
time_percent_of_est: null,
|
|
2248
|
+
color: "pipeline_step",
|
|
2249
|
+
milestone: false,
|
|
2250
|
+
project: null,
|
|
2251
|
+
entity: null,
|
|
2252
|
+
step: ref(st),
|
|
2253
|
+
task_assignees: [],
|
|
2254
|
+
task_reviewers: [],
|
|
2255
|
+
sg_sort_order: (i + 1) * 10,
|
|
2256
|
+
task_template: ref(template),
|
|
2257
|
+
template_task: null,
|
|
2258
|
+
created_at: isoDateTime(-190),
|
|
2259
|
+
updated_at: isoDateTime(-190),
|
|
2260
|
+
created_by: ref(bot),
|
|
2261
|
+
updated_by: ref(bot)
|
|
2262
|
+
});
|
|
2263
|
+
templateTaskId += 1;
|
|
2264
|
+
if (previous) depend(templateTask, previous);
|
|
2265
|
+
previous = templateTask;
|
|
2266
|
+
});
|
|
2267
|
+
return template;
|
|
2268
|
+
});
|
|
2269
|
+
p0.values["tracking_settings"] = { default_task_template: { Shot: {
|
|
2270
|
+
type: "TaskTemplate",
|
|
2271
|
+
id: shotTemplate.id,
|
|
2272
|
+
name: shotTemplate.values["code"],
|
|
2273
|
+
valid: "valid"
|
|
2274
|
+
} } };
|
|
2275
|
+
p1.values["tracking_settings"] = { default_task_template: {} };
|
|
2276
|
+
p2.values["tracking_settings"] = {};
|
|
2277
|
+
for (const p of projects) p.values["task_templates"] = [];
|
|
2278
|
+
p0.values["task_templates"] = [ref(assetTemplate)];
|
|
2279
|
+
assetTemplate.values["projects"] = [ref(p0)];
|
|
2280
|
+
const shotTemplateTasks = (rows.get("Task") ?? []).filter((t) => t.values["task_template"]?.id === shotTemplate.id);
|
|
2281
|
+
for (const shot of shots.filter((sh) => sh.values["sg_sequence"].id === sequences[0]?.id)) {
|
|
2282
|
+
shot.values["task_template"] = ref(shotTemplate);
|
|
2283
|
+
for (const taskRef of shot.values["tasks"]) {
|
|
2284
|
+
const task = index.get(`Task:${taskRef.id}`);
|
|
2285
|
+
const source = shotTemplateTasks.find((t) => t.values["step"].id === task.values["step"].id);
|
|
2286
|
+
task.values["template_task"] = source ? ref(source) : null;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
versions.slice(0, 12).forEach((version, i) => {
|
|
2290
|
+
const code = `${String(version.values["code"])}.exr`;
|
|
2291
|
+
add("PublishedFile", 3e3 + i, {
|
|
2292
|
+
code,
|
|
2293
|
+
cached_display_name: code,
|
|
2294
|
+
name: code.replace(/_v\d+\.exr$/, ".exr"),
|
|
2295
|
+
description: null,
|
|
2296
|
+
version_number: 1,
|
|
2297
|
+
path: null,
|
|
2298
|
+
path_cache: null,
|
|
2299
|
+
sg_status_list: "cmpt",
|
|
2300
|
+
image: null,
|
|
2301
|
+
project: version.values["project"],
|
|
2302
|
+
entity: version.values["entity"],
|
|
2303
|
+
task: version.values["sg_task"],
|
|
2304
|
+
version: ref(version),
|
|
2305
|
+
published_file_type: null,
|
|
2306
|
+
created_at: version.values["created_at"],
|
|
2307
|
+
updated_at: version.values["created_at"],
|
|
2308
|
+
created_by: ref(bot),
|
|
2309
|
+
updated_by: ref(bot)
|
|
2310
|
+
});
|
|
2311
|
+
});
|
|
1983
2312
|
const follows = /* @__PURE__ */ new Map();
|
|
1984
2313
|
const follow = (userRef, row) => {
|
|
1985
2314
|
if (!userRef || userRef.type !== "HumanUser") return;
|
|
@@ -2011,12 +2340,17 @@ var MockClient = class {
|
|
|
2011
2340
|
retired = /* @__PURE__ */ new Map();
|
|
2012
2341
|
/** Rows a batch created with no `project`: no read reaches them, a delete does (report 001). */
|
|
2013
2342
|
unreadable = /* @__PURE__ */ new Map();
|
|
2343
|
+
/** What a Task delete did to other rows, by `Task:id`, for its revive to undo (089_task_delete_side_effects). */
|
|
2344
|
+
cascades = /* @__PURE__ */ new Map();
|
|
2345
|
+
/** The highest id handed out per type, so an erased row's id is never reused. */
|
|
2346
|
+
issued = /* @__PURE__ */ new Map();
|
|
2014
2347
|
deletes = 0;
|
|
2015
2348
|
constructor(options = {}) {
|
|
2016
2349
|
this.fixtures = buildFixtures(options.seed ?? 1, options.counts ?? {});
|
|
2017
2350
|
this.latencyMs = options.latencyMs ?? 0;
|
|
2018
2351
|
this.clock = toClock(options.now);
|
|
2019
2352
|
this.pendingFailure = options.failNext ?? null;
|
|
2353
|
+
this.derive();
|
|
2020
2354
|
}
|
|
2021
2355
|
/**
|
|
2022
2356
|
* Make the next call reject with an `SgApiError`, for demoing an error state.
|
|
@@ -2063,7 +2397,8 @@ var MockClient = class {
|
|
|
2063
2397
|
dataType: s.dataType,
|
|
2064
2398
|
editable: s.editable ?? true,
|
|
2065
2399
|
mandatory: s.mandatory ?? false,
|
|
2066
|
-
unique: s.unique ?? false
|
|
2400
|
+
unique: s.unique ?? false,
|
|
2401
|
+
hideable: HIDEABLE.has(`${entityType}.${name}`)
|
|
2067
2402
|
};
|
|
2068
2403
|
if (s.validTypes) field.validTypes = s.validTypes;
|
|
2069
2404
|
if (s.validValues) field.validValues = s.validValues;
|
|
@@ -2096,6 +2431,14 @@ var MockClient = class {
|
|
|
2096
2431
|
hasMore: page.length === size
|
|
2097
2432
|
};
|
|
2098
2433
|
}
|
|
2434
|
+
async read(entityType, id, options = {}) {
|
|
2435
|
+
await this.gate();
|
|
2436
|
+
const spec = this.schemaOf(entityType);
|
|
2437
|
+
const key = `${entityType}:${id}`;
|
|
2438
|
+
const row = options.retired ? this.retired.get(key) : this.fixtures.index.get(key);
|
|
2439
|
+
if (!row) throw notFound(`${entityType}: ${id} not found`);
|
|
2440
|
+
return this.project(row, spec, options.fields);
|
|
2441
|
+
}
|
|
2099
2442
|
async textSearch(text, entityTypes, page) {
|
|
2100
2443
|
await this.gate();
|
|
2101
2444
|
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2133,21 +2476,31 @@ var MockClient = class {
|
|
|
2133
2476
|
}
|
|
2134
2477
|
async update(entityType, id, patch) {
|
|
2135
2478
|
await this.gate();
|
|
2136
|
-
return this.updateRow(entityType, id, patch);
|
|
2479
|
+
return this.updateRow(entityType, id, patch, false);
|
|
2137
2480
|
}
|
|
2138
|
-
updateRow(entityType, id, patch) {
|
|
2481
|
+
updateRow(entityType, id, patch, inBatch) {
|
|
2139
2482
|
const spec = this.schemaOf(entityType);
|
|
2140
2483
|
const row = this.fixtures.index.get(`${entityType}:${id}`);
|
|
2141
|
-
if (!row) throw
|
|
2484
|
+
if (!row) throw notFound(`Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2142
2485
|
for (const [name, value] of Object.entries(patch)) {
|
|
2143
2486
|
const field = spec[name];
|
|
2144
2487
|
if (!field) throw new SgApiError(400, null, `API update() ${entityType}.${name} doesn't exist.`);
|
|
2145
2488
|
if (field.createOnly) throw new SgApiError(400, null, `API update() ${entityType}.${name} is editable on create only.`);
|
|
2146
|
-
if (field.editable === false) throw new SgApiError(400, null, `API update() ${entityType}.${name} is read only.`);
|
|
2489
|
+
if (field.editable === false && !field.writable) throw new SgApiError(400, null, `API update() ${entityType}.${name} is read only.`);
|
|
2147
2490
|
this.checkLink("update", entityType, name, field, value);
|
|
2148
|
-
|
|
2491
|
+
this.checkValue(inBatch, entityType, name, field, value);
|
|
2492
|
+
}
|
|
2493
|
+
for (const [name, value] of Object.entries(patch)) {
|
|
2494
|
+
const field = spec[name];
|
|
2495
|
+
if (entityType === "Task" && (name === "upstream_tasks" || name === "downstream_tasks")) {
|
|
2496
|
+
this.writeEdges(row, name, value);
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
row.values[name] = (field.dataType === "text" || field.dataType === "status_list") && value === "" ? null : value;
|
|
2500
|
+
this.mirror(row, name);
|
|
2149
2501
|
}
|
|
2150
2502
|
if (spec["updated_at"] && Object.keys(patch).length > 0) row.values["updated_at"] = isoDateTime(0);
|
|
2503
|
+
this.derive();
|
|
2151
2504
|
return this.project(row, spec);
|
|
2152
2505
|
}
|
|
2153
2506
|
/**
|
|
@@ -2493,17 +2846,21 @@ var MockClient = class {
|
|
|
2493
2846
|
*/
|
|
2494
2847
|
createRow(entityType, body, inBatch) {
|
|
2495
2848
|
const spec = this.schemaOf(entityType);
|
|
2496
|
-
const
|
|
2849
|
+
const templateTask = entityType === "Task" && !isNullish(body["task_template"]);
|
|
2850
|
+
const orphan = spec["project"] !== void 0 && body["project"] === void 0 && !templateTask;
|
|
2497
2851
|
if (orphan && !inBatch) throw new SgApiError(400, null, `API create() missing 'project' attribute: ${JSON.stringify(body)}`);
|
|
2852
|
+
if (templateTask && !isNullish(body["project"])) throw new SgApiError(400, null, "API create() Invalid Task: a task template may not have a project");
|
|
2498
2853
|
const identity = IDENTITY_FIELD[entityType];
|
|
2499
2854
|
for (const [name, value] of Object.entries(body)) {
|
|
2500
2855
|
const field = spec[name];
|
|
2501
2856
|
if (!field && inBatch) throw new SgApiError(400, null, `Invalid field value, update failed [2 - Invalid field name: field [${entityType}.${name}] does not exist or user does not have access permission.]`);
|
|
2502
2857
|
if (!field) throw new SgApiError(400, null, `API create() ${entityType}.${name} doesn't exist.`);
|
|
2503
|
-
if (field.editable === false && !field.createOnly) throw new SgApiError(400, null, `API create() ${entityType}.${name} is read only.`);
|
|
2858
|
+
if (field.editable === false && !field.createOnly && !field.writable) throw new SgApiError(400, null, `API create() ${entityType}.${name} is read only.`);
|
|
2504
2859
|
this.checkLink("create", entityType, name, field, value);
|
|
2860
|
+
this.checkValue(true, entityType, name, field, value);
|
|
2505
2861
|
if (name === identity && value === "") throw new SgApiError(400, null, `Create failed for [${entityType}]: Cannot set identifier field to empty. (${entityType})`);
|
|
2506
2862
|
}
|
|
2863
|
+
if (entityType === "TaskDependency") this.checkEdge(body);
|
|
2507
2864
|
const id = this.nextId(entityType);
|
|
2508
2865
|
const values = {};
|
|
2509
2866
|
for (const [name, field] of Object.entries(spec)) values[name] = field.dataType === "multi_entity" ? [] : field.defaultValue ?? null;
|
|
@@ -2517,7 +2874,8 @@ var MockClient = class {
|
|
|
2517
2874
|
values["created_at"] = isoDateTime(0);
|
|
2518
2875
|
if (spec["updated_at"]) values["updated_at"] = isoDateTime(0);
|
|
2519
2876
|
Object.assign(values, body, { id });
|
|
2520
|
-
if (identity && values[identity] === null && GENERATED_IDENTITY.has(entityType)) values[identity] = `New ${DISPLAY_NAMES[entityType] ?? entityType} ${id}`;
|
|
2877
|
+
if (identity && values[identity] === null && GENERATED_IDENTITY.has(entityType)) values[identity] = `New ${GENERATED_NAME[entityType] ?? DISPLAY_NAMES[entityType] ?? entityType} ${id}`;
|
|
2878
|
+
if (entityType === "TaskDependency" && values["dependency_type"] === null) values["dependency_type"] = DEPENDENCY_TYPES[0];
|
|
2521
2879
|
values["cached_display_name"] = entityType === "Note" ? [values["subject"], values["content"]].filter(Boolean).join(" - ") : displayNameOf({
|
|
2522
2880
|
...values,
|
|
2523
2881
|
cached_display_name: null
|
|
@@ -2536,12 +2894,18 @@ var MockClient = class {
|
|
|
2536
2894
|
else this.fixtures.rows.set(entityType, [row]);
|
|
2537
2895
|
this.fixtures.index.set(`${entityType}:${id}`, row);
|
|
2538
2896
|
this.linkBack(row);
|
|
2897
|
+
for (const name of Object.keys(body)) if (entityType === "Task" && (name === "upstream_tasks" || name === "downstream_tasks")) this.writeEdges(row, name, body[name]);
|
|
2898
|
+
else this.mirror(row, name);
|
|
2899
|
+
this.derive();
|
|
2539
2900
|
return this.project(row, spec);
|
|
2540
2901
|
}
|
|
2541
2902
|
/**
|
|
2542
2903
|
* Retire one row: reads skip it and a second delete is 404 (delete_entity_type_id).
|
|
2543
|
-
*
|
|
2544
|
-
*
|
|
2904
|
+
* A Task takes its TaskDependency rows with it and leaves `Version.sg_task` and
|
|
2905
|
+
* `PublishedFile.task` null until its revive (089_task_delete_side_effects), alone or
|
|
2906
|
+
* in a batch (103_batch_delete_revive). A TaskTemplate retires its tasks
|
|
2907
|
+
* (entity_types/TaskTemplate). What a Shot delete does to its Versions (probe 060)
|
|
2908
|
+
* is not modelled.
|
|
2545
2909
|
*/
|
|
2546
2910
|
async delete(entityType, id) {
|
|
2547
2911
|
await this.gate();
|
|
@@ -2551,29 +2915,213 @@ var MockClient = class {
|
|
|
2551
2915
|
this.schemaOf(entityType);
|
|
2552
2916
|
const key = `${entityType}:${id}`;
|
|
2553
2917
|
const row = this.fixtures.index.get(key) ?? this.unreadable.get(key);
|
|
2554
|
-
if (!row) throw
|
|
2918
|
+
if (!row) throw notFound(`Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2919
|
+
if (entityType === "TaskTemplate") for (const task of this.live("Task").filter((t) => refsOf(t.values["task_template"])[0]?.id === id)) this.deleteRow("Task", task.id);
|
|
2920
|
+
if (entityType === "Task") {
|
|
2921
|
+
const cascade = {
|
|
2922
|
+
edges: [],
|
|
2923
|
+
unlinked: []
|
|
2924
|
+
};
|
|
2925
|
+
for (const edge of this.edgesOf(id)) {
|
|
2926
|
+
this.retire(edge);
|
|
2927
|
+
cascade.edges.push(`TaskDependency:${edge.id}`);
|
|
2928
|
+
}
|
|
2929
|
+
for (const [type, field] of [["Version", "sg_task"], ["PublishedFile", "task"]]) for (const other of this.live(type)) {
|
|
2930
|
+
const value = other.values[field];
|
|
2931
|
+
if (value?.type !== "Task" || value.id !== id) continue;
|
|
2932
|
+
other.values[field] = null;
|
|
2933
|
+
cascade.unlinked.push({
|
|
2934
|
+
row: other,
|
|
2935
|
+
field,
|
|
2936
|
+
value
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
this.cascades.set(key, cascade);
|
|
2940
|
+
}
|
|
2555
2941
|
this.unreadable.delete(key);
|
|
2556
|
-
this.
|
|
2557
|
-
|
|
2558
|
-
const at = rows.indexOf(row);
|
|
2559
|
-
if (at >= 0) rows.splice(at, 1);
|
|
2560
|
-
this.retired.set(key, row);
|
|
2942
|
+
this.retire(row);
|
|
2943
|
+
this.derive();
|
|
2561
2944
|
}
|
|
2562
|
-
/**
|
|
2945
|
+
/**
|
|
2946
|
+
* Bring a retired row back with its values; `false` for a row that is already live
|
|
2947
|
+
* (post_entity_type_id). A Task gets back what its delete took (089_task_delete_side_effects).
|
|
2948
|
+
* A TaskDependency whose pair is linked again refuses, the pair being a unique index
|
|
2949
|
+
* (095_dependency_remove_undo). A TaskTemplate's tasks stay retired: unmeasured.
|
|
2950
|
+
*/
|
|
2563
2951
|
async revive(entityType, id) {
|
|
2564
2952
|
await this.gate();
|
|
2565
2953
|
this.schemaOf(entityType);
|
|
2566
2954
|
const key = `${entityType}:${id}`;
|
|
2567
2955
|
if (this.fixtures.index.has(key)) return false;
|
|
2568
2956
|
const row = this.retired.get(key);
|
|
2569
|
-
if (!row) throw
|
|
2957
|
+
if (!row) throw notFound(`Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2958
|
+
if (entityType === "TaskDependency" && this.pairTaken(row)) throw new SgApiError(400, null, `Revive failed for [TaskDependency with id=${id}]: Can't unretire the entity because a field has a non-unique value for a unique index: sgcu_task_dependencies`);
|
|
2959
|
+
this.unretire(row);
|
|
2960
|
+
const cascade = this.cascades.get(key);
|
|
2961
|
+
if (cascade) {
|
|
2962
|
+
this.cascades.delete(key);
|
|
2963
|
+
for (const edgeKey of cascade.edges) {
|
|
2964
|
+
const edge = this.retired.get(edgeKey);
|
|
2965
|
+
if (edge && !this.pairTaken(edge)) this.unretire(edge);
|
|
2966
|
+
}
|
|
2967
|
+
for (const { row: other, field, value } of cascade.unlinked) if (this.fixtures.index.get(`${other.type}:${other.id}`) === other && other.values[field] === null) other.values[field] = value;
|
|
2968
|
+
}
|
|
2969
|
+
this.derive();
|
|
2970
|
+
return true;
|
|
2971
|
+
}
|
|
2972
|
+
retire(row) {
|
|
2973
|
+
const key = `${row.type}:${row.id}`;
|
|
2974
|
+
this.fixtures.index.delete(key);
|
|
2975
|
+
const rows = this.fixtures.rows.get(row.type) ?? [];
|
|
2976
|
+
const at = rows.indexOf(row);
|
|
2977
|
+
if (at >= 0) rows.splice(at, 1);
|
|
2978
|
+
this.retired.set(key, row);
|
|
2979
|
+
}
|
|
2980
|
+
unretire(row) {
|
|
2981
|
+
const key = `${row.type}:${row.id}`;
|
|
2570
2982
|
this.retired.delete(key);
|
|
2571
|
-
const rows = this.fixtures.rows.get(
|
|
2572
|
-
const at = rows.findIndex((r) => r.id > id);
|
|
2983
|
+
const rows = this.fixtures.rows.get(row.type) ?? [];
|
|
2984
|
+
const at = rows.findIndex((r) => r.id > row.id);
|
|
2573
2985
|
rows.splice(at < 0 ? rows.length : at, 0, row);
|
|
2574
|
-
this.fixtures.rows.set(
|
|
2986
|
+
this.fixtures.rows.set(row.type, rows);
|
|
2575
2987
|
this.fixtures.index.set(key, row);
|
|
2576
|
-
|
|
2988
|
+
}
|
|
2989
|
+
/** Remove a row for good: no read reaches it and no revive brings it back. */
|
|
2990
|
+
erase(row) {
|
|
2991
|
+
this.fixtures.index.delete(`${row.type}:${row.id}`);
|
|
2992
|
+
const rows = this.fixtures.rows.get(row.type) ?? [];
|
|
2993
|
+
const at = rows.indexOf(row);
|
|
2994
|
+
if (at >= 0) rows.splice(at, 1);
|
|
2995
|
+
}
|
|
2996
|
+
live(entityType) {
|
|
2997
|
+
return [...this.fixtures.rows.get(entityType) ?? []];
|
|
2998
|
+
}
|
|
2999
|
+
/** The live TaskDependency rows with this Task at either end. */
|
|
3000
|
+
edgesOf(taskId) {
|
|
3001
|
+
return this.live("TaskDependency").filter((d) => refsOf(d.values["task"])[0]?.id === taskId || refsOf(d.values["dependent_task"])[0]?.id === taskId);
|
|
3002
|
+
}
|
|
3003
|
+
/** Whether a live row already links this row's pair. */
|
|
3004
|
+
pairTaken(edge) {
|
|
3005
|
+
const down = refsOf(edge.values["task"])[0]?.id;
|
|
3006
|
+
const up = refsOf(edge.values["dependent_task"])[0]?.id;
|
|
3007
|
+
return this.live("TaskDependency").some((d) => d !== edge && refsOf(d.values["task"])[0]?.id === down && refsOf(d.values["dependent_task"])[0]?.id === up);
|
|
3008
|
+
}
|
|
3009
|
+
/** Whether `from` depends on `target`, directly or through other Tasks. */
|
|
3010
|
+
dependsOn(from, target) {
|
|
3011
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3012
|
+
const queue = [from];
|
|
3013
|
+
while (queue.length > 0) {
|
|
3014
|
+
const at = queue.shift();
|
|
3015
|
+
if (at === target) return true;
|
|
3016
|
+
if (seen.has(at)) continue;
|
|
3017
|
+
seen.add(at);
|
|
3018
|
+
for (const d of this.live("TaskDependency")) if (refsOf(d.values["task"])[0]?.id === at) {
|
|
3019
|
+
const up = refsOf(d.values["dependent_task"])[0]?.id;
|
|
3020
|
+
if (up !== void 0) queue.push(up);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
return false;
|
|
3024
|
+
}
|
|
3025
|
+
/**
|
|
3026
|
+
* The checks a TaskDependency create makes: one of four types, one row per pair, and
|
|
3027
|
+
* no loop of any length (085_task_dependency_types, 107_dependency_three_task_loop).
|
|
3028
|
+
*/
|
|
3029
|
+
checkEdge(body) {
|
|
3030
|
+
const failed = (reason) => new SgApiError(400, null, `Create failed for [TaskDependency]: ${reason}`);
|
|
3031
|
+
if ("dependency_type" in body && !DEPENDENCY_TYPES.includes(body["dependency_type"])) throw failed(`Validation failed: Dependency type Dependency type '${body["dependency_type"] === null ? "" : String(body["dependency_type"])}' is invalid, accepted values are ${DEPENDENCY_TYPES.join(", ")}.`);
|
|
3032
|
+
const down = refsOf(body["task"])[0]?.id;
|
|
3033
|
+
const up = refsOf(body["dependent_task"])[0]?.id;
|
|
3034
|
+
if (down === void 0 || up === void 0) return;
|
|
3035
|
+
if (this.live("TaskDependency").some((d) => refsOf(d.values["task"])[0]?.id === down && refsOf(d.values["dependent_task"])[0]?.id === up)) throw failed("Validation failed: There is already a connection between the entities.");
|
|
3036
|
+
if (down === up || this.dependsOn(up, down)) throw failed("Can't create this dependency as it causes a loop.");
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* A write to `upstream_tasks` or `downstream_tasks`: a new neighbour gets a row of the
|
|
3040
|
+
* default type and no offset, a dropped one's row is erased, not retired
|
|
3041
|
+
* (086_batch_tasks_with_dependencies, 095_dependency_remove_undo).
|
|
3042
|
+
*/
|
|
3043
|
+
writeEdges(task, field, value) {
|
|
3044
|
+
const [own, other] = field === "upstream_tasks" ? ["task", "dependent_task"] : ["dependent_task", "task"];
|
|
3045
|
+
const wanted = refsOf(value).map((r) => r.id);
|
|
3046
|
+
const current = this.live("TaskDependency").filter((d) => refsOf(d.values[own])[0]?.id === task.id);
|
|
3047
|
+
for (const edge of current) if (!wanted.includes(refsOf(edge.values[other])[0]?.id)) this.erase(edge);
|
|
3048
|
+
for (const id of wanted) {
|
|
3049
|
+
if (current.some((d) => refsOf(d.values[other])[0]?.id === id)) continue;
|
|
3050
|
+
const neighbour = {
|
|
3051
|
+
type: "Task",
|
|
3052
|
+
id
|
|
3053
|
+
};
|
|
3054
|
+
const [down, up] = own === "task" ? [ref(task), neighbour] : [neighbour, ref(task)];
|
|
3055
|
+
const edgeId = this.nextId("TaskDependency");
|
|
3056
|
+
const author = this.fixtures.rows.get("ApiUser")?.[0];
|
|
3057
|
+
const edge = {
|
|
3058
|
+
type: "TaskDependency",
|
|
3059
|
+
id: edgeId,
|
|
3060
|
+
values: {
|
|
3061
|
+
id: edgeId,
|
|
3062
|
+
cached_display_name: null,
|
|
3063
|
+
task: down,
|
|
3064
|
+
dependent_task: up,
|
|
3065
|
+
dependency_type: DEPENDENCY_TYPES[0],
|
|
3066
|
+
offset_days: null,
|
|
3067
|
+
shift_ratio: null,
|
|
3068
|
+
task_id: down.id,
|
|
3069
|
+
dependent_task_id: up.id,
|
|
3070
|
+
created_at: isoDateTime(0),
|
|
3071
|
+
updated_at: isoDateTime(0),
|
|
3072
|
+
created_by: author ? ref(author) : null,
|
|
3073
|
+
updated_by: author ? ref(author) : null
|
|
3074
|
+
}
|
|
3075
|
+
};
|
|
3076
|
+
const rows = this.fixtures.rows.get("TaskDependency");
|
|
3077
|
+
if (rows) rows.push(edge);
|
|
3078
|
+
else this.fixtures.rows.set("TaskDependency", [edge]);
|
|
3079
|
+
this.fixtures.index.set(`TaskDependency:${edgeId}`, edge);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
/** Where the written field is one side of a two-sided link, make the other side agree. */
|
|
3083
|
+
mirror(row, name) {
|
|
3084
|
+
const pair = MIRRORED[`${row.type}.${name}`];
|
|
3085
|
+
if (!pair) return;
|
|
3086
|
+
const [otherType, otherField] = pair;
|
|
3087
|
+
const linked = new Set(refsOf(row.values[name]).map((r) => r.id));
|
|
3088
|
+
for (const other of this.live(otherType)) {
|
|
3089
|
+
const list = refsOf(other.values[otherField]).filter((r) => !(r.type === row.type && r.id === row.id));
|
|
3090
|
+
if (linked.has(other.id)) list.push(ref(row));
|
|
3091
|
+
other.values[otherField] = list;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* What the server derives from other rows: a Task's two edge lists from the
|
|
3096
|
+
* TaskDependency rows, their ids and name, and a template's task count, a string.
|
|
3097
|
+
*/
|
|
3098
|
+
derive() {
|
|
3099
|
+
const upstream = /* @__PURE__ */ new Map();
|
|
3100
|
+
const downstream = /* @__PURE__ */ new Map();
|
|
3101
|
+
const push = (map, key, value) => {
|
|
3102
|
+
map.set(key, [...map.get(key) ?? [], {
|
|
3103
|
+
type: "Task",
|
|
3104
|
+
id: value.id
|
|
3105
|
+
}]);
|
|
3106
|
+
};
|
|
3107
|
+
for (const edge of this.live("TaskDependency")) {
|
|
3108
|
+
const down = refsOf(edge.values["task"])[0];
|
|
3109
|
+
const up = refsOf(edge.values["dependent_task"])[0];
|
|
3110
|
+
edge.values["task_id"] = down?.id ?? null;
|
|
3111
|
+
edge.values["dependent_task_id"] = up?.id ?? null;
|
|
3112
|
+
edge.values["cached_display_name"] = `Task ${down?.id ?? ""} dependent on Task ${up?.id ?? ""}`;
|
|
3113
|
+
if (!down || !up) continue;
|
|
3114
|
+
push(upstream, down.id, up);
|
|
3115
|
+
push(downstream, up.id, down);
|
|
3116
|
+
}
|
|
3117
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3118
|
+
for (const task of this.live("Task")) {
|
|
3119
|
+
task.values["upstream_tasks"] = upstream.get(task.id) ?? [];
|
|
3120
|
+
task.values["downstream_tasks"] = downstream.get(task.id) ?? [];
|
|
3121
|
+
const template = refsOf(task.values["task_template"])[0];
|
|
3122
|
+
if (template) counts.set(template.id, (counts.get(template.id) ?? 0) + 1);
|
|
3123
|
+
}
|
|
3124
|
+
for (const template of this.live("TaskTemplate")) template.values["task_count"] = String(counts.get(template.id) ?? 0);
|
|
2577
3125
|
}
|
|
2578
3126
|
/**
|
|
2579
3127
|
* Apply the requests in order, and undo all of them when one fails (recipes/002).
|
|
@@ -2621,7 +3169,7 @@ var MockClient = class {
|
|
|
2621
3169
|
};
|
|
2622
3170
|
return {
|
|
2623
3171
|
request_type: "update",
|
|
2624
|
-
data: this.updateRow(request.entity, id, data)
|
|
3172
|
+
data: this.updateRow(request.entity, id, data, true)
|
|
2625
3173
|
};
|
|
2626
3174
|
}
|
|
2627
3175
|
/** Everything a batch can change, and a function that puts it back. */
|
|
@@ -2631,6 +3179,8 @@ var MockClient = class {
|
|
|
2631
3179
|
const values = new Map([...index.values()].map((row) => [row, structuredClone(row.values)]));
|
|
2632
3180
|
const retired = new Map(this.retired);
|
|
2633
3181
|
const unreadable = new Map(this.unreadable);
|
|
3182
|
+
const cascades = new Map(this.cascades);
|
|
3183
|
+
const issued = new Map(this.issued);
|
|
2634
3184
|
const deletes = this.deletes;
|
|
2635
3185
|
const refill = (target, from) => {
|
|
2636
3186
|
target.clear();
|
|
@@ -2642,6 +3192,8 @@ var MockClient = class {
|
|
|
2642
3192
|
for (const [row, saved] of values) row.values = saved;
|
|
2643
3193
|
refill(this.retired, retired);
|
|
2644
3194
|
refill(this.unreadable, unreadable);
|
|
3195
|
+
refill(this.cascades, cascades);
|
|
3196
|
+
refill(this.issued, issued);
|
|
2645
3197
|
this.deletes = deletes;
|
|
2646
3198
|
};
|
|
2647
3199
|
}
|
|
@@ -2656,7 +3208,7 @@ var MockClient = class {
|
|
|
2656
3208
|
await this.gate();
|
|
2657
3209
|
const spec = this.schemaOf(entityType);
|
|
2658
3210
|
const target = this.fixtures.index.get(`${entityType}:${id}`);
|
|
2659
|
-
if (!target) throw
|
|
3211
|
+
if (!target) throw notFound(`Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2660
3212
|
if (!file.filename) throw new SgApiError(400, { filename: ["filename is missing"] }, "Request Parameters invalid.");
|
|
2661
3213
|
if (file.field !== void 0 && !spec[file.field]) throw new SgApiError(404, null, `Field '${entityType}.${file.field}' does not exist.`);
|
|
2662
3214
|
const attachmentId = this.nextId("Attachment");
|
|
@@ -2730,11 +3282,33 @@ var MockClient = class {
|
|
|
2730
3282
|
}
|
|
2731
3283
|
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)}`);
|
|
2732
3284
|
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Values the server checks beyond their shape. A status code must be in the site-wide
|
|
3287
|
+
* `valid_values`, hidden ones included, and a create or any batch request wraps the
|
|
3288
|
+
* refusal (field_types/status_list, recipes/002, 094_permission_preflight). A
|
|
3289
|
+
* TaskTemplate's `entity_type` must name a type (entity_types/TaskTemplate).
|
|
3290
|
+
*/
|
|
3291
|
+
checkValue(wrapped, entityType, name, field, value) {
|
|
3292
|
+
if (value === null || value === void 0 || value === "") return;
|
|
3293
|
+
let reason = null;
|
|
3294
|
+
if (field.dataType === "status_list" && !(field.validValues ?? []).includes(value)) {
|
|
3295
|
+
const valid = (field.validValues ?? []).map((v) => `'${v}'`).join(", ");
|
|
3296
|
+
reason = `Update failed for [${entityType}.${name}]: '${String(value)}' is not a valid status. Valid statuses: ${valid}.`;
|
|
3297
|
+
}
|
|
3298
|
+
if (field.dataType === "entity_type" && field.writable && !SPECS[value]) {
|
|
3299
|
+
const valid = Object.keys(SPECS).map((v) => `'${v}'`).join(", ");
|
|
3300
|
+
throw new SgApiError(400, null, `Update failed for [${entityType}.${name}]: '${String(value)}' is not a valid entity type. Valid entity types: ${valid}.`);
|
|
3301
|
+
}
|
|
3302
|
+
if (reason === null) return;
|
|
3303
|
+
throw new SgApiError(400, null, wrapped ? `Invalid field value, update failed [5 - ${reason}]` : reason);
|
|
3304
|
+
}
|
|
2733
3305
|
/** The next free id of a type, which is what a create takes. */
|
|
2734
3306
|
nextId(entityType) {
|
|
2735
3307
|
let taken = (this.fixtures.rows.get(entityType) ?? []).reduce((max, row) => Math.max(max, row.id), 0);
|
|
2736
3308
|
for (const row of [...this.retired.values(), ...this.unreadable.values()]) if (row.type === entityType) taken = Math.max(taken, row.id);
|
|
2737
|
-
|
|
3309
|
+
const id = Math.max(taken, this.issued.get(entityType) ?? 0) + 1;
|
|
3310
|
+
this.issued.set(entityType, id);
|
|
3311
|
+
return id;
|
|
2738
3312
|
}
|
|
2739
3313
|
/** The reverse view of a link the server fills in: a Reply lands in `Note.replies`. */
|
|
2740
3314
|
linkBack(row) {
|
|
@@ -3029,6 +3603,19 @@ function entityTypeNamed(name) {
|
|
|
3029
3603
|
const wanted = name.toLowerCase();
|
|
3030
3604
|
return Object.keys(SPECS).find((type) => type.toLowerCase() === wanted || pluralPath(type) === wanted) ?? null;
|
|
3031
3605
|
}
|
|
3606
|
+
/**
|
|
3607
|
+
* The 404 a row that is not there answers: code 104, title `Not Found`, the message in
|
|
3608
|
+
* `detail` (put_entity_type_id, get_entity_notes_id_thread_contents). The message is the
|
|
3609
|
+
* detail, as on every other mock error.
|
|
3610
|
+
*/
|
|
3611
|
+
function notFound(detail) {
|
|
3612
|
+
return new SgApiError(404, { errors: [{
|
|
3613
|
+
status: 404,
|
|
3614
|
+
code: 104,
|
|
3615
|
+
title: "Not Found",
|
|
3616
|
+
detail
|
|
3617
|
+
}] }, detail);
|
|
3618
|
+
}
|
|
3032
3619
|
function isNullish(v) {
|
|
3033
3620
|
return v === null || v === void 0 || v === "";
|
|
3034
3621
|
}
|