@superbuilders/primer-tives 0.9.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 +298 -63
- 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 +338 -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.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +10 -0
- package/dist/server/create-server.d.ts +21 -19
- package/dist/server/create-server.d.ts.map +1 -1
- package/dist/server/exchange.d.ts +10 -5
- package/dist/server/exchange.d.ts.map +1 -1
- package/dist/server/hints.d.ts +25 -0
- package/dist/server/hints.d.ts.map +1 -0
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +355 -83
- package/dist/server/index.js.map +9 -8
- package/dist/server/students.d.ts +3 -5
- package/dist/server/students.d.ts.map +1 -1
- 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");
|
|
@@ -15,6 +12,7 @@ var ErrTimeout = errors.new("timeout");
|
|
|
15
12
|
var ErrForbidden = errors.new("forbidden");
|
|
16
13
|
var ErrNotFound = errors.new("not found");
|
|
17
14
|
var ErrConflict = errors.new("conflict");
|
|
15
|
+
var ErrExternalAuthorityRequired = errors.new("external authority required");
|
|
18
16
|
var ErrRateLimited = errors.new("rate limited");
|
|
19
17
|
var ErrServiceUnavailable = errors.new("service unavailable");
|
|
20
18
|
var ErrNotSerializable = errors.new("PrimerState is live in-memory state and must not be serialized or stored");
|
|
@@ -23,15 +21,323 @@ var ErrInvalidSecretKey = errors.new("invalid secret key");
|
|
|
23
21
|
var ErrStudentNotFound = errors.new("student not found");
|
|
24
22
|
var ErrUnsupportedGrade = errors.new("unsupported grade");
|
|
25
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";
|
|
26
316
|
|
|
27
317
|
// src/client/transport.ts
|
|
28
318
|
import * as errors2 from "@superbuilders/errors";
|
|
29
319
|
var ADVANCE_PATH = "/api/v0/advance";
|
|
30
|
-
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) {
|
|
31
333
|
if (status === 400) {
|
|
32
334
|
return ErrBadRequest;
|
|
33
335
|
}
|
|
34
336
|
if (status === 401) {
|
|
337
|
+
const parsed = parseAdvanceErrorBody(body);
|
|
338
|
+
if (parsed?.detail === "token_expired") {
|
|
339
|
+
return ErrTokenExpired;
|
|
340
|
+
}
|
|
35
341
|
return ErrInvalidAccessToken;
|
|
36
342
|
}
|
|
37
343
|
if (status === 403) {
|
|
@@ -105,7 +411,7 @@ function createTransport(tc) {
|
|
|
105
411
|
const text = await res.text().catch(function fallback() {
|
|
106
412
|
return "";
|
|
107
413
|
});
|
|
108
|
-
const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status);
|
|
414
|
+
const sentinel = res.status === 422 ? ErrUnsupportedPci : httpSentinel(res.status, text);
|
|
109
415
|
log?.error("transport http error", {
|
|
110
416
|
status: res.status
|
|
111
417
|
});
|
|
@@ -140,38 +446,6 @@ function poisonToJSON() {
|
|
|
140
446
|
|
|
141
447
|
// src/client/choice-state.ts
|
|
142
448
|
import * as errors3 from "@superbuilders/errors";
|
|
143
|
-
function findUnknownKey(selectedKeys, options) {
|
|
144
|
-
const optionIds = new Set(options.map(function getId(o) {
|
|
145
|
-
return o.identifier;
|
|
146
|
-
}));
|
|
147
|
-
for (const key of selectedKeys) {
|
|
148
|
-
if (!optionIds.has(key)) {
|
|
149
|
-
return key;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
function validateChoiceSubmission(log, selectedKeys, options, maxChoices, minChoices) {
|
|
155
|
-
if (selectedKeys.length < minChoices) {
|
|
156
|
-
log?.error("choice submit below minChoices", { selectedKeys, minChoices });
|
|
157
|
-
return errors3.wrap(ErrInvalidSubmission, `need at least ${minChoices} selections`);
|
|
158
|
-
}
|
|
159
|
-
if (selectedKeys.length > maxChoices) {
|
|
160
|
-
log?.error("choice submit above maxChoices", { selectedKeys, maxChoices });
|
|
161
|
-
return errors3.wrap(ErrInvalidSubmission, `at most ${maxChoices} selections`);
|
|
162
|
-
}
|
|
163
|
-
const uniqueKeys = new Set(selectedKeys);
|
|
164
|
-
if (uniqueKeys.size !== selectedKeys.length) {
|
|
165
|
-
log?.error("choice submit has duplicates", { selectedKeys });
|
|
166
|
-
return errors3.wrap(ErrInvalidSubmission, "duplicate selections");
|
|
167
|
-
}
|
|
168
|
-
const unknownKey = findUnknownKey(selectedKeys, options);
|
|
169
|
-
if (unknownKey) {
|
|
170
|
-
log?.error("choice submit has unknown key", { key: unknownKey });
|
|
171
|
-
return errors3.wrap(ErrInvalidSubmission, `unknown option '${unknownKey}'`);
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
449
|
function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices) {
|
|
176
450
|
let submitPending;
|
|
177
451
|
let submitKey;
|
|
@@ -188,9 +462,10 @@ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices
|
|
|
188
462
|
}
|
|
189
463
|
return Promise.resolve(ctx.errored(errors3.wrap(ErrConflict, "cannot submit a different choice payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
190
464
|
}
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
|
|
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 }));
|
|
194
469
|
}
|
|
195
470
|
submitKey = key;
|
|
196
471
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -228,17 +503,6 @@ function choiceState(ctx, stimulus, interaction, options, maxChoices, minChoices
|
|
|
228
503
|
|
|
229
504
|
// src/client/extended-text-state.ts
|
|
230
505
|
import * as errors4 from "@superbuilders/errors";
|
|
231
|
-
function validateExtendedTextSubmission(log, values, minStrings, maxStrings) {
|
|
232
|
-
if (values.length < minStrings) {
|
|
233
|
-
log?.error("extended-text submit below minStrings", { count: values.length, minStrings });
|
|
234
|
-
return errors4.wrap(ErrInvalidSubmission, `need at least ${minStrings} values`);
|
|
235
|
-
}
|
|
236
|
-
if (values.length > maxStrings) {
|
|
237
|
-
log?.error("extended-text submit above maxStrings", { count: values.length, maxStrings });
|
|
238
|
-
return errors4.wrap(ErrInvalidSubmission, `at most ${maxStrings} values`);
|
|
239
|
-
}
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
506
|
function extendedTextState(ctx, stimulus, interaction) {
|
|
243
507
|
if (interaction.cardinality === "single") {
|
|
244
508
|
let submitText = function(value) {
|
|
@@ -253,6 +517,11 @@ function extendedTextState(ctx, stimulus, interaction) {
|
|
|
253
517
|
}
|
|
254
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 }));
|
|
255
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
|
+
}
|
|
256
525
|
submitKey2 = key;
|
|
257
526
|
submitPending2 = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
258
527
|
submitPending2 = undefined;
|
|
@@ -302,9 +571,10 @@ function extendedTextState(ctx, stimulus, interaction) {
|
|
|
302
571
|
}
|
|
303
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 }));
|
|
304
573
|
}
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
307
|
-
|
|
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 }));
|
|
308
578
|
}
|
|
309
579
|
submitKey = key;
|
|
310
580
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -364,92 +634,6 @@ function feedbackState(ctx, stimulus, interaction, submission, isCorrect, feedba
|
|
|
364
634
|
|
|
365
635
|
// src/client/match-state.ts
|
|
366
636
|
import * as errors5 from "@superbuilders/errors";
|
|
367
|
-
function bumpCount(counts, key) {
|
|
368
|
-
const prev = counts.get(key);
|
|
369
|
-
const next = prev === undefined ? 1 : prev + 1;
|
|
370
|
-
counts.set(key, next);
|
|
371
|
-
}
|
|
372
|
-
function getCount(counts, key) {
|
|
373
|
-
const recorded = counts.get(key);
|
|
374
|
-
return recorded === undefined ? 0 : recorded;
|
|
375
|
-
}
|
|
376
|
-
function validateAssociationBounds(log, pairs, minAssociations, maxAssociations) {
|
|
377
|
-
if (pairs.length < minAssociations) {
|
|
378
|
-
log?.error("match submit below minAssociations", { pairs, minAssociations });
|
|
379
|
-
return errors5.wrap(ErrInvalidSubmission, `need at least ${minAssociations} associations`);
|
|
380
|
-
}
|
|
381
|
-
if (pairs.length > maxAssociations) {
|
|
382
|
-
log?.error("match submit above maxAssociations", { pairs, maxAssociations });
|
|
383
|
-
return errors5.wrap(ErrInvalidSubmission, `at most ${maxAssociations} associations`);
|
|
384
|
-
}
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
function validatePairsReferToKnownChoices(log, pairs, sourceIds, targetIds) {
|
|
388
|
-
for (const pair of pairs) {
|
|
389
|
-
if (!sourceIds.has(pair.source)) {
|
|
390
|
-
log?.error("match submit has unknown source", { source: pair.source });
|
|
391
|
-
return errors5.wrap(ErrInvalidSubmission, `unknown source '${pair.source}'`);
|
|
392
|
-
}
|
|
393
|
-
if (!targetIds.has(pair.target)) {
|
|
394
|
-
log?.error("match submit has unknown target", { target: pair.target });
|
|
395
|
-
return errors5.wrap(ErrInvalidSubmission, `unknown target '${pair.target}'`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
function validateChoiceCaps(log, side, choices, counts) {
|
|
401
|
-
for (const choice of choices) {
|
|
402
|
-
const count = getCount(counts, choice.identifier);
|
|
403
|
-
if (choice.matchMax !== 0 && count > choice.matchMax) {
|
|
404
|
-
log?.error(`match submit exceeds ${side} matchMax`, {
|
|
405
|
-
identifier: choice.identifier,
|
|
406
|
-
count,
|
|
407
|
-
matchMax: choice.matchMax
|
|
408
|
-
});
|
|
409
|
-
return errors5.wrap(ErrInvalidSubmission, `${side} '${choice.identifier}' used ${count} times, max ${choice.matchMax}`);
|
|
410
|
-
}
|
|
411
|
-
if (count < choice.matchMin) {
|
|
412
|
-
log?.error(`match submit below ${side} matchMin`, {
|
|
413
|
-
identifier: choice.identifier,
|
|
414
|
-
count,
|
|
415
|
-
matchMin: choice.matchMin
|
|
416
|
-
});
|
|
417
|
-
return errors5.wrap(ErrInvalidSubmission, `${side} '${choice.identifier}' used ${count} times, min ${choice.matchMin}`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
function validateMatchSubmission(log, pairs, sourceChoices, targetChoices, minAssociations, maxAssociations) {
|
|
423
|
-
const boundsError = validateAssociationBounds(log, pairs, minAssociations, maxAssociations);
|
|
424
|
-
if (boundsError) {
|
|
425
|
-
return boundsError;
|
|
426
|
-
}
|
|
427
|
-
const sourceIds = new Set(sourceChoices.map(function getId(c) {
|
|
428
|
-
return c.identifier;
|
|
429
|
-
}));
|
|
430
|
-
const targetIds = new Set(targetChoices.map(function getId(c) {
|
|
431
|
-
return c.identifier;
|
|
432
|
-
}));
|
|
433
|
-
const referenceError = validatePairsReferToKnownChoices(log, pairs, sourceIds, targetIds);
|
|
434
|
-
if (referenceError) {
|
|
435
|
-
return referenceError;
|
|
436
|
-
}
|
|
437
|
-
const sourceCounts = new Map;
|
|
438
|
-
const targetCounts = new Map;
|
|
439
|
-
for (const pair of pairs) {
|
|
440
|
-
bumpCount(sourceCounts, pair.source);
|
|
441
|
-
bumpCount(targetCounts, pair.target);
|
|
442
|
-
}
|
|
443
|
-
const sourceCapError = validateChoiceCaps(log, "source", sourceChoices, sourceCounts);
|
|
444
|
-
if (sourceCapError) {
|
|
445
|
-
return sourceCapError;
|
|
446
|
-
}
|
|
447
|
-
const targetCapError = validateChoiceCaps(log, "target", targetChoices, targetCounts);
|
|
448
|
-
if (targetCapError) {
|
|
449
|
-
return targetCapError;
|
|
450
|
-
}
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
637
|
function matchState(ctx, stimulus, interaction) {
|
|
454
638
|
let submitPending;
|
|
455
639
|
let submitKey;
|
|
@@ -466,9 +650,10 @@ function matchState(ctx, stimulus, interaction) {
|
|
|
466
650
|
}
|
|
467
651
|
return Promise.resolve(ctx.errored(errors5.wrap(ErrConflict, "cannot submit a different match payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
468
652
|
}
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
471
|
-
|
|
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 }));
|
|
472
657
|
}
|
|
473
658
|
submitKey = key;
|
|
474
659
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -524,38 +709,6 @@ function observationState(ctx, stimulus) {
|
|
|
524
709
|
|
|
525
710
|
// src/client/order-state.ts
|
|
526
711
|
import * as errors6 from "@superbuilders/errors";
|
|
527
|
-
function findUnknownKey2(orderedKeys, choices) {
|
|
528
|
-
const choiceIds = new Set(choices.map(function getId(c) {
|
|
529
|
-
return c.identifier;
|
|
530
|
-
}));
|
|
531
|
-
for (const key of orderedKeys) {
|
|
532
|
-
if (!choiceIds.has(key)) {
|
|
533
|
-
return key;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
function validateOrderSubmission(log, orderedKeys, choices, minChoices, maxChoices) {
|
|
539
|
-
if (orderedKeys.length < minChoices) {
|
|
540
|
-
log?.error("order submit below minChoices", { orderedKeys, minChoices });
|
|
541
|
-
return errors6.wrap(ErrInvalidSubmission, `need at least ${minChoices} selections`);
|
|
542
|
-
}
|
|
543
|
-
if (orderedKeys.length > maxChoices) {
|
|
544
|
-
log?.error("order submit above maxChoices", { orderedKeys, maxChoices });
|
|
545
|
-
return errors6.wrap(ErrInvalidSubmission, `at most ${maxChoices} selections`);
|
|
546
|
-
}
|
|
547
|
-
const uniqueKeys = new Set(orderedKeys);
|
|
548
|
-
if (uniqueKeys.size !== orderedKeys.length) {
|
|
549
|
-
log?.error("order submit has duplicates", { orderedKeys });
|
|
550
|
-
return errors6.wrap(ErrInvalidSubmission, "duplicate selections");
|
|
551
|
-
}
|
|
552
|
-
const unknownKey = findUnknownKey2(orderedKeys, choices);
|
|
553
|
-
if (unknownKey) {
|
|
554
|
-
log?.error("order submit has unknown key", { key: unknownKey });
|
|
555
|
-
return errors6.wrap(ErrInvalidSubmission, `unknown choice '${unknownKey}'`);
|
|
556
|
-
}
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
712
|
function orderState(ctx, stimulus, interaction) {
|
|
560
713
|
let submitPending;
|
|
561
714
|
let submitKey;
|
|
@@ -572,9 +725,10 @@ function orderState(ctx, stimulus, interaction) {
|
|
|
572
725
|
}
|
|
573
726
|
return Promise.resolve(ctx.errored(errors6.wrap(ErrConflict, "cannot submit a different order payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
574
727
|
}
|
|
575
|
-
const
|
|
576
|
-
if (
|
|
577
|
-
|
|
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 }));
|
|
578
732
|
}
|
|
579
733
|
submitKey = key;
|
|
580
734
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
@@ -682,6 +836,11 @@ function textEntryState(ctx, stimulus, interaction) {
|
|
|
682
836
|
}
|
|
683
837
|
return Promise.resolve(ctx.errored(errors8.wrap(ErrConflict, "cannot submit a different text payload while submit is in flight"), "interaction", { kind: "interaction", submission }));
|
|
684
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
|
+
}
|
|
685
844
|
submitKey = key;
|
|
686
845
|
submitPending = ctx.execute({ kind: "interaction", submission }, "interaction").finally(function clearPending() {
|
|
687
846
|
submitPending = undefined;
|
|
@@ -923,4 +1082,4 @@ export {
|
|
|
923
1082
|
ErrBadRequest
|
|
924
1083
|
};
|
|
925
1084
|
|
|
926
|
-
//# debugId=
|
|
1085
|
+
//# debugId=BFF38D60446A9E1864756E2164756E21
|