sg-widgets-core 0.1.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/{client-BhHppzTV.d.mts → client-CP9sWEVZ.d.mts} +106 -1
- package/dist/{client-DwN_-UuF.mjs → client-izKszX7l.mjs} +42 -1
- package/dist/index.d.mts +100 -3
- package/dist/index.mjs +261 -5
- package/dist/mock.d.mts +79 -1
- package/dist/mock.mjs +731 -13
- package/package.json +1 -1
package/dist/mock.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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;
|
|
@@ -2007,11 +2336,21 @@ var MockClient = class {
|
|
|
2007
2336
|
latencyMs;
|
|
2008
2337
|
clock;
|
|
2009
2338
|
pendingFailure;
|
|
2339
|
+
/** Rows a delete retired, by `Type:id`. Reads skip them and a revive puts them back. */
|
|
2340
|
+
retired = /* @__PURE__ */ new Map();
|
|
2341
|
+
/** Rows a batch created with no `project`: no read reaches them, a delete does (report 001). */
|
|
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();
|
|
2347
|
+
deletes = 0;
|
|
2010
2348
|
constructor(options = {}) {
|
|
2011
2349
|
this.fixtures = buildFixtures(options.seed ?? 1, options.counts ?? {});
|
|
2012
2350
|
this.latencyMs = options.latencyMs ?? 0;
|
|
2013
2351
|
this.clock = toClock(options.now);
|
|
2014
2352
|
this.pendingFailure = options.failNext ?? null;
|
|
2353
|
+
this.derive();
|
|
2015
2354
|
}
|
|
2016
2355
|
/**
|
|
2017
2356
|
* Make the next call reject with an `SgApiError`, for demoing an error state.
|
|
@@ -2058,7 +2397,8 @@ var MockClient = class {
|
|
|
2058
2397
|
dataType: s.dataType,
|
|
2059
2398
|
editable: s.editable ?? true,
|
|
2060
2399
|
mandatory: s.mandatory ?? false,
|
|
2061
|
-
unique: s.unique ?? false
|
|
2400
|
+
unique: s.unique ?? false,
|
|
2401
|
+
hideable: HIDEABLE.has(`${entityType}.${name}`)
|
|
2062
2402
|
};
|
|
2063
2403
|
if (s.validTypes) field.validTypes = s.validTypes;
|
|
2064
2404
|
if (s.validValues) field.validValues = s.validValues;
|
|
@@ -2091,6 +2431,14 @@ var MockClient = class {
|
|
|
2091
2431
|
hasMore: page.length === size
|
|
2092
2432
|
};
|
|
2093
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
|
+
}
|
|
2094
2442
|
async textSearch(text, entityTypes, page) {
|
|
2095
2443
|
await this.gate();
|
|
2096
2444
|
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2128,18 +2476,31 @@ var MockClient = class {
|
|
|
2128
2476
|
}
|
|
2129
2477
|
async update(entityType, id, patch) {
|
|
2130
2478
|
await this.gate();
|
|
2479
|
+
return this.updateRow(entityType, id, patch, false);
|
|
2480
|
+
}
|
|
2481
|
+
updateRow(entityType, id, patch, inBatch) {
|
|
2131
2482
|
const spec = this.schemaOf(entityType);
|
|
2132
2483
|
const row = this.fixtures.index.get(`${entityType}:${id}`);
|
|
2133
|
-
if (!row) throw
|
|
2484
|
+
if (!row) throw notFound(`Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2134
2485
|
for (const [name, value] of Object.entries(patch)) {
|
|
2135
2486
|
const field = spec[name];
|
|
2136
2487
|
if (!field) throw new SgApiError(400, null, `API update() ${entityType}.${name} doesn't exist.`);
|
|
2137
2488
|
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.`);
|
|
2489
|
+
if (field.editable === false && !field.writable) throw new SgApiError(400, null, `API update() ${entityType}.${name} is read only.`);
|
|
2139
2490
|
this.checkLink("update", entityType, name, field, value);
|
|
2140
|
-
|
|
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);
|
|
2141
2501
|
}
|
|
2142
2502
|
if (spec["updated_at"] && Object.keys(patch).length > 0) row.values["updated_at"] = isoDateTime(0);
|
|
2503
|
+
this.derive();
|
|
2143
2504
|
return this.project(row, spec);
|
|
2144
2505
|
}
|
|
2145
2506
|
/**
|
|
@@ -2476,16 +2837,30 @@ var MockClient = class {
|
|
|
2476
2837
|
*/
|
|
2477
2838
|
async create(entityType, body) {
|
|
2478
2839
|
await this.gate();
|
|
2840
|
+
return this.createRow(entityType, body, false);
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* A create, alone or inside a batch. The batch skips the `project` check and stores
|
|
2844
|
+
* the row where no read reaches it (report 001), and it spells an unknown field its
|
|
2845
|
+
* own way (recipes/002).
|
|
2846
|
+
*/
|
|
2847
|
+
createRow(entityType, body, inBatch) {
|
|
2479
2848
|
const spec = this.schemaOf(entityType);
|
|
2480
|
-
|
|
2849
|
+
const templateTask = entityType === "Task" && !isNullish(body["task_template"]);
|
|
2850
|
+
const orphan = spec["project"] !== void 0 && body["project"] === void 0 && !templateTask;
|
|
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");
|
|
2481
2853
|
const identity = IDENTITY_FIELD[entityType];
|
|
2482
2854
|
for (const [name, value] of Object.entries(body)) {
|
|
2483
2855
|
const field = spec[name];
|
|
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.]`);
|
|
2484
2857
|
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.`);
|
|
2858
|
+
if (field.editable === false && !field.createOnly && !field.writable) throw new SgApiError(400, null, `API create() ${entityType}.${name} is read only.`);
|
|
2486
2859
|
this.checkLink("create", entityType, name, field, value);
|
|
2860
|
+
this.checkValue(true, entityType, name, field, value);
|
|
2487
2861
|
if (name === identity && value === "") throw new SgApiError(400, null, `Create failed for [${entityType}]: Cannot set identifier field to empty. (${entityType})`);
|
|
2488
2862
|
}
|
|
2863
|
+
if (entityType === "TaskDependency") this.checkEdge(body);
|
|
2489
2864
|
const id = this.nextId(entityType);
|
|
2490
2865
|
const values = {};
|
|
2491
2866
|
for (const [name, field] of Object.entries(spec)) values[name] = field.dataType === "multi_entity" ? [] : field.defaultValue ?? null;
|
|
@@ -2499,7 +2874,8 @@ var MockClient = class {
|
|
|
2499
2874
|
values["created_at"] = isoDateTime(0);
|
|
2500
2875
|
if (spec["updated_at"]) values["updated_at"] = isoDateTime(0);
|
|
2501
2876
|
Object.assign(values, body, { id });
|
|
2502
|
-
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];
|
|
2503
2879
|
values["cached_display_name"] = entityType === "Note" ? [values["subject"], values["content"]].filter(Boolean).join(" - ") : displayNameOf({
|
|
2504
2880
|
...values,
|
|
2505
2881
|
cached_display_name: null
|
|
@@ -2509,14 +2885,319 @@ var MockClient = class {
|
|
|
2509
2885
|
id,
|
|
2510
2886
|
values
|
|
2511
2887
|
};
|
|
2888
|
+
if (orphan) {
|
|
2889
|
+
this.unreadable.set(`${entityType}:${id}`, row);
|
|
2890
|
+
return this.project(row, spec);
|
|
2891
|
+
}
|
|
2512
2892
|
const rows = this.fixtures.rows.get(entityType);
|
|
2513
2893
|
if (rows) rows.push(row);
|
|
2514
2894
|
else this.fixtures.rows.set(entityType, [row]);
|
|
2515
2895
|
this.fixtures.index.set(`${entityType}:${id}`, row);
|
|
2516
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();
|
|
2517
2900
|
return this.project(row, spec);
|
|
2518
2901
|
}
|
|
2519
2902
|
/**
|
|
2903
|
+
* Retire one row: reads skip it and a second delete is 404 (delete_entity_type_id).
|
|
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.
|
|
2909
|
+
*/
|
|
2910
|
+
async delete(entityType, id) {
|
|
2911
|
+
await this.gate();
|
|
2912
|
+
this.deleteRow(entityType, id);
|
|
2913
|
+
}
|
|
2914
|
+
deleteRow(entityType, id) {
|
|
2915
|
+
this.schemaOf(entityType);
|
|
2916
|
+
const key = `${entityType}:${id}`;
|
|
2917
|
+
const row = this.fixtures.index.get(key) ?? this.unreadable.get(key);
|
|
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
|
+
}
|
|
2941
|
+
this.unreadable.delete(key);
|
|
2942
|
+
this.retire(row);
|
|
2943
|
+
this.derive();
|
|
2944
|
+
}
|
|
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
|
+
*/
|
|
2951
|
+
async revive(entityType, id) {
|
|
2952
|
+
await this.gate();
|
|
2953
|
+
this.schemaOf(entityType);
|
|
2954
|
+
const key = `${entityType}:${id}`;
|
|
2955
|
+
if (this.fixtures.index.has(key)) return false;
|
|
2956
|
+
const row = this.retired.get(key);
|
|
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}`;
|
|
2982
|
+
this.retired.delete(key);
|
|
2983
|
+
const rows = this.fixtures.rows.get(row.type) ?? [];
|
|
2984
|
+
const at = rows.findIndex((r) => r.id > row.id);
|
|
2985
|
+
rows.splice(at < 0 ? rows.length : at, 0, row);
|
|
2986
|
+
this.fixtures.rows.set(row.type, rows);
|
|
2987
|
+
this.fixtures.index.set(key, row);
|
|
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);
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Apply the requests in order, and undo all of them when one fails (recipes/002).
|
|
3128
|
+
* A create or update row carries the record under `data`; a delete row is flat.
|
|
3129
|
+
*/
|
|
3130
|
+
async batch(requests) {
|
|
3131
|
+
await this.gate();
|
|
3132
|
+
const restore = this.snapshot();
|
|
3133
|
+
try {
|
|
3134
|
+
return requests.map((request, i) => this.batchOne(request, i));
|
|
3135
|
+
} catch (error) {
|
|
3136
|
+
restore();
|
|
3137
|
+
throw error;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
batchOne(request, i) {
|
|
3141
|
+
const invalid = (source) => new SgApiError(400, { errors: [{
|
|
3142
|
+
status: 400,
|
|
3143
|
+
code: 103,
|
|
3144
|
+
title: "Request Parameters invalid.",
|
|
3145
|
+
source
|
|
3146
|
+
}] }, "Request Parameters invalid.");
|
|
3147
|
+
const kind = request.request_type;
|
|
3148
|
+
if (kind !== "create" && kind !== "update" && kind !== "delete") throw invalid({ requests: { [i]: { request_type: ["request_type must be one of: create, update, delete"] } } });
|
|
3149
|
+
if (!request.entity) throw invalid({ requests: { [i]: { entity: ["entity is missing"] } } });
|
|
3150
|
+
if (!SPECS[request.entity]) throw new SgApiError(400, null, "Invalid entity type: entity type [] does not exist.");
|
|
3151
|
+
const id = "record_id" in request ? request.record_id ?? 0 : 0;
|
|
3152
|
+
if (request.request_type === "delete") {
|
|
3153
|
+
this.deleteRow(request.entity, id);
|
|
3154
|
+
this.deletes += 1;
|
|
3155
|
+
const uuid = `00000000-0000-4000-8000-${this.deletes.toString(16).padStart(12, "0")}`;
|
|
3156
|
+
return {
|
|
3157
|
+
request_type: "delete",
|
|
3158
|
+
type: request.entity,
|
|
3159
|
+
id,
|
|
3160
|
+
uuid,
|
|
3161
|
+
did_delete: true
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
const data = request.data;
|
|
3165
|
+
if (data === null || typeof data !== "object") throw invalid({ data: ["data hash containing field/value pairs is required for the given request"] });
|
|
3166
|
+
if (request.request_type === "create") return {
|
|
3167
|
+
request_type: "create",
|
|
3168
|
+
data: this.createRow(request.entity, data, true)
|
|
3169
|
+
};
|
|
3170
|
+
return {
|
|
3171
|
+
request_type: "update",
|
|
3172
|
+
data: this.updateRow(request.entity, id, data, true)
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
/** Everything a batch can change, and a function that puts it back. */
|
|
3176
|
+
snapshot() {
|
|
3177
|
+
const rows = new Map([...this.fixtures.rows].map(([type, list]) => [type, [...list]]));
|
|
3178
|
+
const index = new Map(this.fixtures.index);
|
|
3179
|
+
const values = new Map([...index.values()].map((row) => [row, structuredClone(row.values)]));
|
|
3180
|
+
const retired = new Map(this.retired);
|
|
3181
|
+
const unreadable = new Map(this.unreadable);
|
|
3182
|
+
const cascades = new Map(this.cascades);
|
|
3183
|
+
const issued = new Map(this.issued);
|
|
3184
|
+
const deletes = this.deletes;
|
|
3185
|
+
const refill = (target, from) => {
|
|
3186
|
+
target.clear();
|
|
3187
|
+
for (const [k, v] of from) target.set(k, v);
|
|
3188
|
+
};
|
|
3189
|
+
return () => {
|
|
3190
|
+
refill(this.fixtures.rows, rows);
|
|
3191
|
+
refill(this.fixtures.index, index);
|
|
3192
|
+
for (const [row, saved] of values) row.values = saved;
|
|
3193
|
+
refill(this.retired, retired);
|
|
3194
|
+
refill(this.unreadable, unreadable);
|
|
3195
|
+
refill(this.cascades, cascades);
|
|
3196
|
+
refill(this.issued, issued);
|
|
3197
|
+
this.deletes = deletes;
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
2520
3201
|
* Put a file on a row, as the three-call handshake leaves the site.
|
|
2521
3202
|
*
|
|
2522
3203
|
* The field in the path picks the kind: `image` a Thumbnail, another field an
|
|
@@ -2527,7 +3208,7 @@ var MockClient = class {
|
|
|
2527
3208
|
await this.gate();
|
|
2528
3209
|
const spec = this.schemaOf(entityType);
|
|
2529
3210
|
const target = this.fixtures.index.get(`${entityType}:${id}`);
|
|
2530
|
-
if (!target) throw
|
|
3211
|
+
if (!target) throw notFound(`Entity of type [${entityType}] with id=${id} does not exist.`);
|
|
2531
3212
|
if (!file.filename) throw new SgApiError(400, { filename: ["filename is missing"] }, "Request Parameters invalid.");
|
|
2532
3213
|
if (file.field !== void 0 && !spec[file.field]) throw new SgApiError(404, null, `Field '${entityType}.${file.field}' does not exist.`);
|
|
2533
3214
|
const attachmentId = this.nextId("Attachment");
|
|
@@ -2601,9 +3282,33 @@ var MockClient = class {
|
|
|
2601
3282
|
}
|
|
2602
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)}`);
|
|
2603
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
|
+
}
|
|
2604
3305
|
/** The next free id of a type, which is what a create takes. */
|
|
2605
3306
|
nextId(entityType) {
|
|
2606
|
-
|
|
3307
|
+
let taken = (this.fixtures.rows.get(entityType) ?? []).reduce((max, row) => Math.max(max, row.id), 0);
|
|
3308
|
+
for (const row of [...this.retired.values(), ...this.unreadable.values()]) if (row.type === entityType) taken = Math.max(taken, row.id);
|
|
3309
|
+
const id = Math.max(taken, this.issued.get(entityType) ?? 0) + 1;
|
|
3310
|
+
this.issued.set(entityType, id);
|
|
3311
|
+
return id;
|
|
2607
3312
|
}
|
|
2608
3313
|
/** The reverse view of a link the server fills in: a Reply lands in `Note.replies`. */
|
|
2609
3314
|
linkBack(row) {
|
|
@@ -2898,6 +3603,19 @@ function entityTypeNamed(name) {
|
|
|
2898
3603
|
const wanted = name.toLowerCase();
|
|
2899
3604
|
return Object.keys(SPECS).find((type) => type.toLowerCase() === wanted || pluralPath(type) === wanted) ?? null;
|
|
2900
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
|
+
}
|
|
2901
3619
|
function isNullish(v) {
|
|
2902
3620
|
return v === null || v === void 0 || v === "";
|
|
2903
3621
|
}
|