@mohasinac/appkit 4.41.3 → 4.41.5
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.
|
@@ -18,6 +18,7 @@ import { NextResponse } from "next/server.js";
|
|
|
18
18
|
import { getProviders } from "../../../../contracts";
|
|
19
19
|
import { EVENT_FIELDS } from "../../schemas";
|
|
20
20
|
import { normalizeError } from "../../../../errors/normalize";
|
|
21
|
+
import { toClientLotteryConfig } from "../../../../_internal/server/features/lottery/adapters";
|
|
21
22
|
// --- GET /api/events/[id] -----------------------------------------------------
|
|
22
23
|
export async function GET(_request, context) {
|
|
23
24
|
try {
|
|
@@ -33,8 +34,22 @@ export async function GET(_request, context) {
|
|
|
33
34
|
event.status === EVENT_FIELDS.STATUS_VALUES.PAUSED) {
|
|
34
35
|
return NextResponse.json({ success: false, error: "Event not found" }, { status: 404 });
|
|
35
36
|
}
|
|
36
|
-
// Strip internal field
|
|
37
|
-
|
|
37
|
+
// Strip internal field.
|
|
38
|
+
//
|
|
39
|
+
// 🛑 This is a deny-list spread, so anything NOT named here is published by
|
|
40
|
+
// default — which is how a lottery event's stored `lotteryConfig` reached
|
|
41
|
+
// anonymous callers whole. `toClientLotteryConfig` is the allow-list that
|
|
42
|
+
// belongs on this boundary: it drops the per-slot `price` and `weight` (the
|
|
43
|
+
// weighting is how the odds are set) and the internal `bookedByUserId`,
|
|
44
|
+
// while deliberately keeping `bookedByDisplayName` and
|
|
45
|
+
// `bookedByUserLotteryNumber`, which the public slot grid renders.
|
|
46
|
+
const { createdBy: _createdBy, ...rawPublicEvent } = event;
|
|
47
|
+
const publicEvent = rawPublicEvent.lotteryConfig
|
|
48
|
+
? {
|
|
49
|
+
...rawPublicEvent,
|
|
50
|
+
lotteryConfig: toClientLotteryConfig(rawPublicEvent.lotteryConfig),
|
|
51
|
+
}
|
|
52
|
+
: rawPublicEvent;
|
|
38
53
|
const entriesRepo = db.getRepository("eventEntries");
|
|
39
54
|
// For poll events with resultsVisibility='always': compute live vote tallies
|
|
40
55
|
let pollResults = [];
|
|
@@ -162,8 +162,17 @@ function stringMaxLength(schema) {
|
|
|
162
162
|
*/
|
|
163
163
|
function enumOptions(schema) {
|
|
164
164
|
const direct = schema.options;
|
|
165
|
-
if (Array.isArray(direct))
|
|
166
|
-
|
|
165
|
+
if (Array.isArray(direct)) {
|
|
166
|
+
const strings = direct.filter((o) => typeof o === "string");
|
|
167
|
+
// On a ZodUnion `.options` is the member SCHEMAS, not strings, so the
|
|
168
|
+
// filter above yields []. Empty is truthy downstream, which is how a union
|
|
169
|
+
// rendered as a <select> with nothing in it. Read the literals instead.
|
|
170
|
+
if (strings.length === 0) {
|
|
171
|
+
const literals = unionStringLiterals(direct);
|
|
172
|
+
return literals.length > 0 ? literals : undefined;
|
|
173
|
+
}
|
|
174
|
+
return strings;
|
|
175
|
+
}
|
|
167
176
|
const values = schema._def?.values;
|
|
168
177
|
if (Array.isArray(values))
|
|
169
178
|
return values.filter((o) => typeof o === "string");
|
|
@@ -172,6 +181,26 @@ function enumOptions(schema) {
|
|
|
172
181
|
}
|
|
173
182
|
return undefined;
|
|
174
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* String literal values carried by a union's members.
|
|
186
|
+
*
|
|
187
|
+
* `z.literal("a").or(z.literal("b"))` is a genuine choice and yields
|
|
188
|
+
* `["a", "b"]`. `z.string().optional().or(z.literal(""))` — the idiomatic
|
|
189
|
+
* "optional text that also accepts empty" — yields nothing usable, because its
|
|
190
|
+
* only literal is the empty string. That distinction is what decides whether a
|
|
191
|
+
* union field is a select or a text input.
|
|
192
|
+
*/
|
|
193
|
+
function unionStringLiterals(members) {
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const member of members) {
|
|
196
|
+
const direct = member?.value;
|
|
197
|
+
const nested = member?._def?.value;
|
|
198
|
+
const value = typeof direct === "string" ? direct : nested;
|
|
199
|
+
if (typeof value === "string" && value !== "")
|
|
200
|
+
out.push(value);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
175
204
|
/** Public: the option list for a select-shaped field, or null. */
|
|
176
205
|
export function schemaEnumOptions(schema) {
|
|
177
206
|
const { schema: inner } = unwrapSchema(schema);
|
|
@@ -180,7 +209,27 @@ export function schemaEnumOptions(schema) {
|
|
|
180
209
|
/** Public: whether a field's type is optional-wrapped (i.e. not required). */
|
|
181
210
|
export function schemaIsOptional(schema) {
|
|
182
211
|
const type = typeOf(schema);
|
|
183
|
-
|
|
212
|
+
if (type === "optional" || type === "default" || type === "nullish")
|
|
213
|
+
return true;
|
|
214
|
+
// A union HIDES optionality from the check above: the outer type is "union",
|
|
215
|
+
// so `z.string().optional().or(z.literal(""))` read as required and rendered
|
|
216
|
+
// a `*` on a field the schema is happy to leave empty. If any member is
|
|
217
|
+
// optional, or accepts "", the field is optional.
|
|
218
|
+
if (type === "union") {
|
|
219
|
+
const members = schema.options;
|
|
220
|
+
if (Array.isArray(members)) {
|
|
221
|
+
return members.some((member) => {
|
|
222
|
+
const memberType = typeOf(member);
|
|
223
|
+
if (memberType === "optional" || memberType === "default" || memberType === "nullish") {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
const direct = member?.value;
|
|
227
|
+
const nested = member?._def?.value;
|
|
228
|
+
return direct === "" || nested === "";
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
184
233
|
}
|
|
185
234
|
/**
|
|
186
235
|
* The `.shape` of an object schema, seeing through effects wrappers.
|
|
@@ -257,8 +306,19 @@ function fromZodType(type, inner) {
|
|
|
257
306
|
case "enum":
|
|
258
307
|
case "nativeEnum":
|
|
259
308
|
case "literal":
|
|
260
|
-
case "union":
|
|
261
309
|
return { kind: "select", row: "pair" };
|
|
310
|
+
case "union": {
|
|
311
|
+
// Only a union that actually yields choices is a select. The other
|
|
312
|
+
// common shape, `z.string().optional().or(z.literal(""))`, is an optional
|
|
313
|
+
// TEXT field — rendering it as a select produced a dropdown with zero
|
|
314
|
+
// options, which is unusable and (being marked required) made the whole
|
|
315
|
+
// form unsubmittable. Seen live on /user/addresses/new, where
|
|
316
|
+
// "Address line 2" and "Landmark" were both required empty dropdowns.
|
|
317
|
+
const choices = enumOptions(inner);
|
|
318
|
+
return choices && choices.length > 0
|
|
319
|
+
? { kind: "select", row: "pair" }
|
|
320
|
+
: { kind: "text", row: "pair" };
|
|
321
|
+
}
|
|
262
322
|
case "date":
|
|
263
323
|
return { kind: "date", row: "pair" };
|
|
264
324
|
case "number":
|