@superbuilders/primer-tives 1.0.0 → 1.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/README.md +66 -7
- package/dist/client/choice-state.d.ts.map +1 -1
- package/dist/client/extended-text-state.d.ts.map +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +337 -179
- package/dist/client/index.js.map +13 -11
- package/dist/client/match-state.d.ts.map +1 -1
- package/dist/client/order-state.d.ts.map +1 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/text-entry-state.d.ts.map +1 -1
- package/dist/client/transport.d.ts +1 -1
- package/dist/client/transport.d.ts.map +1 -1
- package/dist/client/types.d.ts +2 -115
- package/dist/client/types.d.ts.map +1 -1
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +305 -0
- package/dist/contracts/index.js.map +11 -0
- package/dist/contracts/pci-schemas.d.ts +25 -0
- package/dist/contracts/pci-schemas.d.ts.map +1 -0
- package/dist/contracts/types.d.ts +118 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/validation.d.ts +132 -0
- package/dist/contracts/validation.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +10 -0
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -5
- package/dist/server/index.js.map +5 -5
- package/package.json +17 -4
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/client/create.ts
|
|
2
|
-
import * as errors10 from "@superbuilders/errors";
|
|
3
|
-
|
|
4
1
|
// src/errors.ts
|
|
5
2
|
import * as errors from "@superbuilders/errors";
|
|
6
3
|
var ErrNetwork = errors.new("network");
|
|
@@ -24,15 +21,323 @@ var ErrInvalidSecretKey = errors.new("invalid secret key");
|
|
|
24
21
|
var ErrStudentNotFound = errors.new("student not found");
|
|
25
22
|
var ErrUnsupportedGrade = errors.new("unsupported grade");
|
|
26
23
|
var ErrTimebackUnavailable = errors.new("timeback unavailable");
|
|
24
|
+
// src/contracts/validation.ts
|
|
25
|
+
import { z as z2 } from "zod";
|
|
26
|
+
|
|
27
|
+
// src/contracts/pci-schemas.ts
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
var DivisionRemainderPropsSchema = z.object({
|
|
30
|
+
dividend: z.number(),
|
|
31
|
+
divisor: z.number()
|
|
32
|
+
});
|
|
33
|
+
var DivisionRemainderSubmissionSchema = z.object({
|
|
34
|
+
quotient: z.string(),
|
|
35
|
+
remainder: z.string()
|
|
36
|
+
});
|
|
37
|
+
var FractionOperandSchema = z.object({
|
|
38
|
+
numerator: z.number(),
|
|
39
|
+
denominator: z.number()
|
|
40
|
+
});
|
|
41
|
+
var FractionAdditionPropsSchema = z.object({
|
|
42
|
+
left: FractionOperandSchema,
|
|
43
|
+
right: FractionOperandSchema
|
|
44
|
+
});
|
|
45
|
+
var FractionAdditionSubmissionSchema = z.object({
|
|
46
|
+
numerator: z.string(),
|
|
47
|
+
denominator: z.string()
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// src/contracts/validation.ts
|
|
51
|
+
var MatchPairSchema = z2.object({
|
|
52
|
+
source: z2.string(),
|
|
53
|
+
target: z2.string()
|
|
54
|
+
});
|
|
55
|
+
var ChoiceSubmissionSchema = z2.object({
|
|
56
|
+
type: z2.literal("choice"),
|
|
57
|
+
selectedKeys: z2.array(z2.string())
|
|
58
|
+
});
|
|
59
|
+
var TextEntrySubmissionSchema = z2.object({
|
|
60
|
+
type: z2.literal("text-entry"),
|
|
61
|
+
value: z2.string()
|
|
62
|
+
});
|
|
63
|
+
var ExtendedTextSubmissionSchema = z2.object({
|
|
64
|
+
type: z2.literal("extended-text"),
|
|
65
|
+
values: z2.array(z2.string()).min(1)
|
|
66
|
+
});
|
|
67
|
+
var OrderSubmissionSchema = z2.object({
|
|
68
|
+
type: z2.literal("order"),
|
|
69
|
+
orderedKeys: z2.array(z2.string())
|
|
70
|
+
});
|
|
71
|
+
var MatchSubmissionSchema = z2.object({
|
|
72
|
+
type: z2.literal("match"),
|
|
73
|
+
pairs: z2.array(MatchPairSchema)
|
|
74
|
+
});
|
|
75
|
+
var DivisionRemainderPciSubmissionSchema = z2.object({
|
|
76
|
+
type: z2.literal("portable-custom"),
|
|
77
|
+
pciId: z2.literal("urn:primer:pci:division-remainder"),
|
|
78
|
+
value: DivisionRemainderSubmissionSchema
|
|
79
|
+
});
|
|
80
|
+
var FractionAdditionPciSubmissionSchema = z2.object({
|
|
81
|
+
type: z2.literal("portable-custom"),
|
|
82
|
+
pciId: z2.literal("urn:primer:pci:fraction-addition"),
|
|
83
|
+
value: FractionAdditionSubmissionSchema
|
|
84
|
+
});
|
|
85
|
+
var RendererSubmissionSchema = z2.union([
|
|
86
|
+
ChoiceSubmissionSchema,
|
|
87
|
+
TextEntrySubmissionSchema,
|
|
88
|
+
ExtendedTextSubmissionSchema,
|
|
89
|
+
OrderSubmissionSchema,
|
|
90
|
+
MatchSubmissionSchema,
|
|
91
|
+
DivisionRemainderPciSubmissionSchema,
|
|
92
|
+
FractionAdditionPciSubmissionSchema
|
|
93
|
+
]);
|
|
94
|
+
function invalid(issues) {
|
|
95
|
+
return { ok: false, issues: Array.isArray(issues) ? issues : [issues] };
|
|
96
|
+
}
|
|
97
|
+
function valid(submission) {
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
value: submission
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function duplicates(values, keyOf) {
|
|
104
|
+
const seen = new Set;
|
|
105
|
+
const duplicated = new Set;
|
|
106
|
+
for (const value of values) {
|
|
107
|
+
const key = keyOf(value);
|
|
108
|
+
if (seen.has(key)) {
|
|
109
|
+
duplicated.add(key);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
seen.add(key);
|
|
113
|
+
}
|
|
114
|
+
return [...duplicated];
|
|
115
|
+
}
|
|
116
|
+
function findUnknownIds(values, choices) {
|
|
117
|
+
const ids = new Set(choices.map(function getId(choice) {
|
|
118
|
+
return choice.identifier;
|
|
119
|
+
}));
|
|
120
|
+
return values.filter(function isUnknown(value) {
|
|
121
|
+
return !ids.has(value);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function countByIdentifier(pairs, side) {
|
|
125
|
+
const counts = new Map;
|
|
126
|
+
for (const pair of pairs) {
|
|
127
|
+
const key = pair[side];
|
|
128
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
return counts;
|
|
131
|
+
}
|
|
132
|
+
function validateUsageBounds(choices, counts, side) {
|
|
133
|
+
const issues = [];
|
|
134
|
+
for (const choice of choices) {
|
|
135
|
+
const count = counts.get(choice.identifier) ?? 0;
|
|
136
|
+
if (choice.matchMax !== 0 && count > choice.matchMax) {
|
|
137
|
+
issues.push(`${side} '${choice.identifier}' used ${count} times, max ${choice.matchMax}`);
|
|
138
|
+
}
|
|
139
|
+
if (count < choice.matchMin) {
|
|
140
|
+
issues.push(`${side} '${choice.identifier}' used ${count} times, min ${choice.matchMin}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return issues;
|
|
144
|
+
}
|
|
145
|
+
function pciSubmissionSchema(pciId) {
|
|
146
|
+
switch (pciId) {
|
|
147
|
+
case "urn:primer:pci:division-remainder":
|
|
148
|
+
return DivisionRemainderSubmissionSchema;
|
|
149
|
+
case "urn:primer:pci:fraction-addition":
|
|
150
|
+
return FractionAdditionSubmissionSchema;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function validateChoiceSubmission(interaction, submission) {
|
|
154
|
+
if (submission.type !== "choice") {
|
|
155
|
+
return invalid(`submission type '${submission.type}' does not match interaction type 'choice'`);
|
|
156
|
+
}
|
|
157
|
+
const issues = [];
|
|
158
|
+
if (submission.selectedKeys.length < interaction.minChoices) {
|
|
159
|
+
issues.push(`need at least ${interaction.minChoices} selections`);
|
|
160
|
+
}
|
|
161
|
+
if (submission.selectedKeys.length > interaction.maxChoices) {
|
|
162
|
+
issues.push(`at most ${interaction.maxChoices} selections`);
|
|
163
|
+
}
|
|
164
|
+
const duplicateKeys = duplicates(submission.selectedKeys, function keyOf(value) {
|
|
165
|
+
return value;
|
|
166
|
+
});
|
|
167
|
+
if (duplicateKeys.length > 0) {
|
|
168
|
+
issues.push("duplicate selections");
|
|
169
|
+
}
|
|
170
|
+
const unknownKeys = findUnknownIds(submission.selectedKeys, interaction.options);
|
|
171
|
+
if (unknownKeys.length > 0) {
|
|
172
|
+
issues.push(`unknown options: ${unknownKeys.join(", ")}`);
|
|
173
|
+
}
|
|
174
|
+
if (issues.length > 0) {
|
|
175
|
+
return invalid(issues);
|
|
176
|
+
}
|
|
177
|
+
return valid(submission);
|
|
178
|
+
}
|
|
179
|
+
function validateTextEntrySubmission(interaction, submission) {
|
|
180
|
+
if (submission.type !== "text-entry") {
|
|
181
|
+
return invalid(`submission type '${submission.type}' does not match interaction type 'text-entry'`);
|
|
182
|
+
}
|
|
183
|
+
return valid(submission);
|
|
184
|
+
}
|
|
185
|
+
function validateExtendedTextSubmission(interaction, submission) {
|
|
186
|
+
if (submission.type !== "extended-text") {
|
|
187
|
+
return invalid(`submission type '${submission.type}' does not match interaction type 'extended-text'`);
|
|
188
|
+
}
|
|
189
|
+
const issues = [];
|
|
190
|
+
if (interaction.cardinality === "single") {
|
|
191
|
+
if (submission.values.length !== 1) {
|
|
192
|
+
issues.push("single-cardinality extended-text requires exactly one value");
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
if (submission.values.length < interaction.minStrings) {
|
|
196
|
+
issues.push(`need at least ${interaction.minStrings} values`);
|
|
197
|
+
}
|
|
198
|
+
if (submission.values.length > interaction.maxStrings) {
|
|
199
|
+
issues.push(`at most ${interaction.maxStrings} values`);
|
|
200
|
+
}
|
|
201
|
+
const duplicateValues = duplicates(submission.values, function keyOf(value) {
|
|
202
|
+
return value;
|
|
203
|
+
});
|
|
204
|
+
if (duplicateValues.length > 0) {
|
|
205
|
+
issues.push("duplicate values are not allowed for multiple-cardinality extended-text");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (issues.length > 0) {
|
|
209
|
+
return invalid(issues);
|
|
210
|
+
}
|
|
211
|
+
return valid(submission);
|
|
212
|
+
}
|
|
213
|
+
function validateOrderSubmission(interaction, submission) {
|
|
214
|
+
if (submission.type !== "order") {
|
|
215
|
+
return invalid(`submission type '${submission.type}' does not match interaction type 'order'`);
|
|
216
|
+
}
|
|
217
|
+
const issues = [];
|
|
218
|
+
if (submission.orderedKeys.length < interaction.minChoices) {
|
|
219
|
+
issues.push(`need at least ${interaction.minChoices} selections`);
|
|
220
|
+
}
|
|
221
|
+
if (submission.orderedKeys.length > interaction.maxChoices) {
|
|
222
|
+
issues.push(`at most ${interaction.maxChoices} selections`);
|
|
223
|
+
}
|
|
224
|
+
const duplicateKeys = duplicates(submission.orderedKeys, function keyOf(value) {
|
|
225
|
+
return value;
|
|
226
|
+
});
|
|
227
|
+
if (duplicateKeys.length > 0) {
|
|
228
|
+
issues.push("duplicate selections");
|
|
229
|
+
}
|
|
230
|
+
const unknownKeys = findUnknownIds(submission.orderedKeys, interaction.choices);
|
|
231
|
+
if (unknownKeys.length > 0) {
|
|
232
|
+
issues.push(`unknown choices: ${unknownKeys.join(", ")}`);
|
|
233
|
+
}
|
|
234
|
+
if (issues.length > 0) {
|
|
235
|
+
return invalid(issues);
|
|
236
|
+
}
|
|
237
|
+
return valid(submission);
|
|
238
|
+
}
|
|
239
|
+
function validateMatchSubmission(interaction, submission) {
|
|
240
|
+
if (submission.type !== "match") {
|
|
241
|
+
return invalid(`submission type '${submission.type}' does not match interaction type 'match'`);
|
|
242
|
+
}
|
|
243
|
+
const issues = [];
|
|
244
|
+
if (submission.pairs.length < interaction.minAssociations) {
|
|
245
|
+
issues.push(`need at least ${interaction.minAssociations} associations`);
|
|
246
|
+
}
|
|
247
|
+
if (submission.pairs.length > interaction.maxAssociations) {
|
|
248
|
+
issues.push(`at most ${interaction.maxAssociations} associations`);
|
|
249
|
+
}
|
|
250
|
+
const duplicatePairs = duplicates(submission.pairs, function keyOf(pair) {
|
|
251
|
+
return `${pair.source}->${pair.target}`;
|
|
252
|
+
});
|
|
253
|
+
if (duplicatePairs.length > 0) {
|
|
254
|
+
issues.push("duplicate associations are not allowed");
|
|
255
|
+
}
|
|
256
|
+
const sourceIds = new Set(interaction.sourceChoices.map(function getId(choice) {
|
|
257
|
+
return choice.identifier;
|
|
258
|
+
}));
|
|
259
|
+
const targetIds = new Set(interaction.targetChoices.map(function getId(choice) {
|
|
260
|
+
return choice.identifier;
|
|
261
|
+
}));
|
|
262
|
+
for (const pair of submission.pairs) {
|
|
263
|
+
if (!sourceIds.has(pair.source)) {
|
|
264
|
+
issues.push(`unknown source '${pair.source}'`);
|
|
265
|
+
}
|
|
266
|
+
if (!targetIds.has(pair.target)) {
|
|
267
|
+
issues.push(`unknown target '${pair.target}'`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const sourceCounts = countByIdentifier(submission.pairs, "source");
|
|
271
|
+
const targetCounts = countByIdentifier(submission.pairs, "target");
|
|
272
|
+
issues.push(...validateUsageBounds(interaction.sourceChoices, sourceCounts, "source"));
|
|
273
|
+
issues.push(...validateUsageBounds(interaction.targetChoices, targetCounts, "target"));
|
|
274
|
+
if (issues.length > 0) {
|
|
275
|
+
return invalid(issues);
|
|
276
|
+
}
|
|
277
|
+
return valid(submission);
|
|
278
|
+
}
|
|
279
|
+
function validatePortableCustomSubmission(interaction, submission) {
|
|
280
|
+
if (submission.type !== "portable-custom") {
|
|
281
|
+
return invalid(`submission type '${submission.type}' does not match interaction type 'portable-custom'`);
|
|
282
|
+
}
|
|
283
|
+
if (submission.pciId !== interaction.pciId) {
|
|
284
|
+
return invalid(`submission PCI '${submission.pciId}' does not match interaction PCI '${interaction.pciId}'`);
|
|
285
|
+
}
|
|
286
|
+
const schema = pciSubmissionSchema(interaction.pciId);
|
|
287
|
+
const result = schema.safeParse(submission.value);
|
|
288
|
+
if (!result.success) {
|
|
289
|
+
return invalid(result.error.issues.map(function toIssue(issue) {
|
|
290
|
+
return issue.message;
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
return valid(submission);
|
|
294
|
+
}
|
|
295
|
+
function validateSubmissionForInteraction(interaction, submission) {
|
|
296
|
+
switch (interaction.type) {
|
|
297
|
+
case "choice":
|
|
298
|
+
return validateChoiceSubmission(interaction, submission);
|
|
299
|
+
case "text-entry":
|
|
300
|
+
return validateTextEntrySubmission(interaction, submission);
|
|
301
|
+
case "extended-text":
|
|
302
|
+
return validateExtendedTextSubmission(interaction, submission);
|
|
303
|
+
case "order":
|
|
304
|
+
return validateOrderSubmission(interaction, submission);
|
|
305
|
+
case "match":
|
|
306
|
+
return validateMatchSubmission(interaction, submission);
|
|
307
|
+
case "portable-custom":
|
|
308
|
+
return validatePortableCustomSubmission(interaction, submission);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function submissionValidationMessage(result) {
|
|
312
|
+
return result.issues.join("; ");
|
|
313
|
+
}
|
|
314
|
+
// src/client/create.ts
|
|
315
|
+
import * as errors10 from "@superbuilders/errors";
|
|
27
316
|
|
|
28
317
|
// src/client/transport.ts
|
|
29
318
|
import * as errors2 from "@superbuilders/errors";
|
|
30
319
|
var ADVANCE_PATH = "/api/v0/advance";
|
|
31
|
-
function
|
|
320
|
+
function parseAdvanceErrorBody(body) {
|
|
321
|
+
if (body.length === 0) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const parsed = errors2.trySync(function parseJson() {
|
|
325
|
+
return JSON.parse(body);
|
|
326
|
+
});
|
|
327
|
+
if (parsed.error) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return parsed.data;
|
|
331
|
+
}
|
|
332
|
+
function httpSentinel(status, body) {
|
|
32
333
|
if (status === 400) {
|
|
33
334
|
return ErrBadRequest;
|
|
34
335
|
}
|
|
35
336
|
if (status === 401) {
|
|
337
|
+
const parsed = parseAdvanceErrorBody(body);
|
|
338
|
+
if (parsed?.detail === "token_expired") {
|
|
339
|
+
return ErrTokenExpired;
|
|
340
|
+
}
|
|
36
341
|
return ErrInvalidAccessToken;
|
|
37
342
|
}
|
|
38
343
|
if (status === 403) {
|
|
@@ -106,7 +411,7 @@ function createTransport(tc) {
|
|
|
106
411
|
const text = await res.text().catch(function fallback() {
|
|
107
412
|
return "";
|
|
108
413
|
});
|
|
109
|
-
const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status);
|
|
414
|
+
const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status, text);
|
|
110
415
|
log?.error("transport http error", {
|
|
111
416
|
status: res.status
|
|
112
417
|
});
|
|
@@ -141,38 +446,6 @@ function poisonToJSON() {
|
|
|
141
446
|
|
|
142
447
|
// src/client/choice-state.ts
|
|
143
448
|
import * as errors3 from "@superbuilders/errors";
|
|
144
|
-
function findUnknownKey(selectedKeys, options) {
|
|
145
|
-
const optionIds = new Set(options.map(function getId(o) {
|
|
146
|
-
return o.identifier;
|
|
147
|
-
}));
|
|
148
|
-
for (const key of selectedKeys) {
|
|
149
|
-
if (!optionIds.has(key)) {
|
|
150
|
-
return key;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
function validateChoiceSubmission(log, selectedKeys, options, maxChoices, minChoices) {
|
|
156
|
-
if (selectedKeys.length < minChoices) {
|
|
157
|
-
log?.error("choice submit below minChoices", { selectedKeys, minChoices });
|
|
158
|
-
return errors3.wrap(ErrInvalidSubmission, `need at least ${minChoices} selections`);
|
|
159
|
-
}
|
|
160
|
-
if (selectedKeys.length > maxChoices) {
|
|
161
|
-
log?.error("choice submit above maxChoices", { selectedKeys, maxChoices });
|
|
162
|
-
return errors3.wrap(ErrInvalidSubmission, `at most ${maxChoices} selections`);
|
|
163
|
-
}
|
|
164
|
-
const uniqueKeys = new Set(selectedKeys);
|
|
165
|
-
if (uniqueKeys.size !== selectedKeys.length) {
|
|
166
|
-
log?.error("choice submit has duplicates", { selectedKeys });
|
|
167
|
-
return errors3.wrap(ErrInvalidSubmission, "duplicate selections");
|
|
168
|
-
}
|
|
169
|
-
const unknownKey = findUnknownKey(selectedKeys, options);
|
|
170
|
-
if (unknownKey) {
|
|
171
|
-
log?.error("choice submit has unknown key", { key: unknownKey });
|
|
172
|
-
return errors3.wrap(ErrInvalidSubmission, `unknown option '${unknownKey}'`);
|
|
173
|
-
}
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
449
|
function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices) {
|
|
177
450
|
let submitPending;
|
|
178
451
|
let submitKey;
|
|
@@ -189,9 +462,10 @@ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices
|
|
|
189
462
|
}
|
|
190
463
|
return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot submit a different choice payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
191
464
|
}
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
|
|
465
|
+
const validation = validateSubmissionForInteraction(interaction, submission);
|
|
466
|
+
if (!validation.ok) {
|
|
467
|
+
ctx.log?.error("choice submit invalid", { selectedKeys, issues: validation.issues });
|
|
468
|
+
return Promise.resolve(ctx.errored(errors3.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
|
|
195
469
|
}
|
|
196
470
|
submitKey = key;
|
|
197
471
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -229,17 +503,6 @@ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices
|
|
|
229
503
|
|
|
230
504
|
// src/client/extended-text-state.ts
|
|
231
505
|
import * as errors4 from "@superbuilders/errors";
|
|
232
|
-
function validateExtendedTextSubmission(log, values, minStrings, maxStrings) {
|
|
233
|
-
if (values.length < minStrings) {
|
|
234
|
-
log?.error("extended-text submit below minStrings", { count: values.length, minStrings });
|
|
235
|
-
return errors4.wrap(ErrInvalidSubmission, `need at least ${minStrings} values`);
|
|
236
|
-
}
|
|
237
|
-
if (values.length > maxStrings) {
|
|
238
|
-
log?.error("extended-text submit above maxStrings", { count: values.length, maxStrings });
|
|
239
|
-
return errors4.wrap(ErrInvalidSubmission, `at most ${maxStrings} values`);
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
506
|
function extendedTextState(ctx, stimulus, interaction) {
|
|
244
507
|
if (interaction.cardinality === "single") {
|
|
245
508
|
let submitText = function(value) {
|
|
@@ -254,6 +517,11 @@ function extendedTextState(ctx, stimulus, interaction) {
|
|
|
254
517
|
}
|
|
255
518
|
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
256
519
|
}
|
|
520
|
+
const validation = validateSubmissionForInteraction(interaction, submission);
|
|
521
|
+
if (!validation.ok) {
|
|
522
|
+
ctx.log?.error("extended-text submit invalid", { value, issues: validation.issues });
|
|
523
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
|
|
524
|
+
}
|
|
257
525
|
submitKey2 = key;
|
|
258
526
|
submitPending2 = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
259
527
|
submitPending2 = undefined;
|
|
@@ -303,9 +571,10 @@ function extendedTextState(ctx, stimulus, interaction) {
|
|
|
303
571
|
}
|
|
304
572
|
return Promise.resolve(ctx.errored(errors4.wrap(ErrConflict, "cannot submit a different extended-text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
305
573
|
}
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
308
|
-
|
|
574
|
+
const validation = validateSubmissionForInteraction(multi, submission);
|
|
575
|
+
if (!validation.ok) {
|
|
576
|
+
ctx.log?.error("extended-text submit invalid", { values, issues: validation.issues });
|
|
577
|
+
return Promise.resolve(ctx.errored(errors4.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
|
|
309
578
|
}
|
|
310
579
|
submitKey = key;
|
|
311
580
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -365,92 +634,6 @@ function feedbackState(ctx, stimulus, interaction, submission, isCorrect, feedba
|
|
|
365
634
|
|
|
366
635
|
// src/client/match-state.ts
|
|
367
636
|
import * as errors5 from "@superbuilders/errors";
|
|
368
|
-
function bumpCount(counts, key) {
|
|
369
|
-
const prev = counts.get(key);
|
|
370
|
-
const next = prev === undefined ? 1 : prev + 1;
|
|
371
|
-
counts.set(key, next);
|
|
372
|
-
}
|
|
373
|
-
function getCount(counts, key) {
|
|
374
|
-
const recorded = counts.get(key);
|
|
375
|
-
return recorded === undefined ? 0 : recorded;
|
|
376
|
-
}
|
|
377
|
-
function validateAssociationBounds(log, pairs, minAssociations, maxAssociations) {
|
|
378
|
-
if (pairs.length < minAssociations) {
|
|
379
|
-
log?.error("match submit below minAssociations", { pairs, minAssociations });
|
|
380
|
-
return errors5.wrap(ErrInvalidSubmission, `need at least ${minAssociations} associations`);
|
|
381
|
-
}
|
|
382
|
-
if (pairs.length > maxAssociations) {
|
|
383
|
-
log?.error("match submit above maxAssociations", { pairs, maxAssociations });
|
|
384
|
-
return errors5.wrap(ErrInvalidSubmission, `at most ${maxAssociations} associations`);
|
|
385
|
-
}
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
function validatePairsReferToKnownChoices(log, pairs, sourceIds, targetIds) {
|
|
389
|
-
for (const pair of pairs) {
|
|
390
|
-
if (!sourceIds.has(pair.source)) {
|
|
391
|
-
log?.error("match submit has unknown source", { source: pair.source });
|
|
392
|
-
return errors5.wrap(ErrInvalidSubmission, `unknown source '${pair.source}'`);
|
|
393
|
-
}
|
|
394
|
-
if (!targetIds.has(pair.target)) {
|
|
395
|
-
log?.error("match submit has unknown target", { target: pair.target });
|
|
396
|
-
return errors5.wrap(ErrInvalidSubmission, `unknown target '${pair.target}'`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
function validateChoiceCaps(log, side, choices, counts) {
|
|
402
|
-
for (const choice of choices) {
|
|
403
|
-
const count = getCount(counts, choice.identifier);
|
|
404
|
-
if (choice.matchMax !== 0 && count > choice.matchMax) {
|
|
405
|
-
log?.error(`match submit exceeds ${side} matchMax`, {
|
|
406
|
-
identifier: choice.identifier,
|
|
407
|
-
count,
|
|
408
|
-
matchMax: choice.matchMax
|
|
409
|
-
});
|
|
410
|
-
return errors5.wrap(ErrInvalidSubmission, `${side} '${choice.identifier}' used ${count} times, max ${choice.matchMax}`);
|
|
411
|
-
}
|
|
412
|
-
if (count < choice.matchMin) {
|
|
413
|
-
log?.error(`match submit below ${side} matchMin`, {
|
|
414
|
-
identifier: choice.identifier,
|
|
415
|
-
count,
|
|
416
|
-
matchMin: choice.matchMin
|
|
417
|
-
});
|
|
418
|
-
return errors5.wrap(ErrInvalidSubmission, `${side} '${choice.identifier}' used ${count} times, min ${choice.matchMin}`);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
function validateMatchSubmission(log, pairs, sourceChoices, targetChoices, minAssociations, maxAssociations) {
|
|
424
|
-
const boundsError = validateAssociationBounds(log, pairs, minAssociations, maxAssociations);
|
|
425
|
-
if (boundsError) {
|
|
426
|
-
return boundsError;
|
|
427
|
-
}
|
|
428
|
-
const sourceIds = new Set(sourceChoices.map(function getId(c) {
|
|
429
|
-
return c.identifier;
|
|
430
|
-
}));
|
|
431
|
-
const targetIds = new Set(targetChoices.map(function getId(c) {
|
|
432
|
-
return c.identifier;
|
|
433
|
-
}));
|
|
434
|
-
const referenceError = validatePairsReferToKnownChoices(log, pairs, sourceIds, targetIds);
|
|
435
|
-
if (referenceError) {
|
|
436
|
-
return referenceError;
|
|
437
|
-
}
|
|
438
|
-
const sourceCounts = new Map;
|
|
439
|
-
const targetCounts = new Map;
|
|
440
|
-
for (const pair of pairs) {
|
|
441
|
-
bumpCount(sourceCounts, pair.source);
|
|
442
|
-
bumpCount(targetCounts, pair.target);
|
|
443
|
-
}
|
|
444
|
-
const sourceCapError = validateChoiceCaps(log, "source", sourceChoices, sourceCounts);
|
|
445
|
-
if (sourceCapError) {
|
|
446
|
-
return sourceCapError;
|
|
447
|
-
}
|
|
448
|
-
const targetCapError = validateChoiceCaps(log, "target", targetChoices, targetCounts);
|
|
449
|
-
if (targetCapError) {
|
|
450
|
-
return targetCapError;
|
|
451
|
-
}
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
637
|
function matchState(ctx, stimulus, interaction) {
|
|
455
638
|
let submitPending;
|
|
456
639
|
let submitKey;
|
|
@@ -467,9 +650,10 @@ function matchState(ctx, stimulus, interaction) {
|
|
|
467
650
|
}
|
|
468
651
|
return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different match payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
469
652
|
}
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
|
|
653
|
+
const validation = validateSubmissionForInteraction(interaction, submission);
|
|
654
|
+
if (!validation.ok) {
|
|
655
|
+
ctx.log?.error("match submit invalid", { pairs, issues: validation.issues });
|
|
656
|
+
return Promise.resolve(ctx.errored(errors5.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
|
|
473
657
|
}
|
|
474
658
|
submitKey = key;
|
|
475
659
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -525,38 +709,6 @@ function observationState(ctx, stimulus) {
|
|
|
525
709
|
|
|
526
710
|
// src/client/order-state.ts
|
|
527
711
|
import * as errors6 from "@superbuilders/errors";
|
|
528
|
-
function findUnknownKey2(orderedKeys, choices) {
|
|
529
|
-
const choiceIds = new Set(choices.map(function getId(c) {
|
|
530
|
-
return c.identifier;
|
|
531
|
-
}));
|
|
532
|
-
for (const key of orderedKeys) {
|
|
533
|
-
if (!choiceIds.has(key)) {
|
|
534
|
-
return key;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
function validateOrderSubmission(log, orderedKeys, choices, minChoices, maxChoices) {
|
|
540
|
-
if (orderedKeys.length < minChoices) {
|
|
541
|
-
log?.error("order submit below minChoices", { orderedKeys, minChoices });
|
|
542
|
-
return errors6.wrap(ErrInvalidSubmission, `need at least ${minChoices} selections`);
|
|
543
|
-
}
|
|
544
|
-
if (orderedKeys.length > maxChoices) {
|
|
545
|
-
log?.error("order submit above maxChoices", { orderedKeys, maxChoices });
|
|
546
|
-
return errors6.wrap(ErrInvalidSubmission, `at most ${maxChoices} selections`);
|
|
547
|
-
}
|
|
548
|
-
const uniqueKeys = new Set(orderedKeys);
|
|
549
|
-
if (uniqueKeys.size !== orderedKeys.length) {
|
|
550
|
-
log?.error("order submit has duplicates", { orderedKeys });
|
|
551
|
-
return errors6.wrap(ErrInvalidSubmission, "duplicate selections");
|
|
552
|
-
}
|
|
553
|
-
const unknownKey = findUnknownKey2(orderedKeys, choices);
|
|
554
|
-
if (unknownKey) {
|
|
555
|
-
log?.error("order submit has unknown key", { key: unknownKey });
|
|
556
|
-
return errors6.wrap(ErrInvalidSubmission, `unknown choice '${unknownKey}'`);
|
|
557
|
-
}
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
712
|
function orderState(ctx, stimulus, interaction) {
|
|
561
713
|
let submitPending;
|
|
562
714
|
let submitKey;
|
|
@@ -573,9 +725,10 @@ function orderState(ctx, stimulus, interaction) {
|
|
|
573
725
|
}
|
|
574
726
|
return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different order payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
575
727
|
}
|
|
576
|
-
const
|
|
577
|
-
if (
|
|
578
|
-
|
|
728
|
+
const validation = validateSubmissionForInteraction(interaction, submission);
|
|
729
|
+
if (!validation.ok) {
|
|
730
|
+
ctx.log?.error("order submit invalid", { orderedKeys, issues: validation.issues });
|
|
731
|
+
return Promise.resolve(ctx.errored(errors6.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
|
|
579
732
|
}
|
|
580
733
|
submitKey = key;
|
|
581
734
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -683,6 +836,11 @@ function textEntryState(ctx, stimulus, interaction) {
|
|
|
683
836
|
}
|
|
684
837
|
return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit a different text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
685
838
|
}
|
|
839
|
+
const validation = validateSubmissionForInteraction(interaction, submission);
|
|
840
|
+
if (!validation.ok) {
|
|
841
|
+
ctx.log?.error("text-entry submit invalid", { value, issues: validation.issues });
|
|
842
|
+
return Promise.resolve(ctx.errored(errors8.wrap(ErrInvalidSubmission, submissionValidationMessage(validation)), "interaction", { kind: "interaction", submission }));
|
|
843
|
+
}
|
|
686
844
|
submitKey = key;
|
|
687
845
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
688
846
|
submitPending = undefined;
|
|
@@ -924,4 +1082,4 @@ export {
|
|
|
924
1082
|
ErrBadRequest
|
|
925
1083
|
};
|
|
926
1084
|
|
|
927
|
-
//# debugId=
|
|
1085
|
+
//# debugId=BFF38D60446A9E1864756E2164756E21
|