@mohasinac/appkit 4.41.5 โ 4.41.6
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/_internal/server/features/events/adapters.d.ts +46 -0
- package/dist/_internal/server/features/events/adapters.js +51 -0
- package/dist/_internal/server/features/lottery/adapters.d.ts +9 -3
- package/dist/_internal/server/features/lottery/adapters.js +7 -1
- package/dist/features/events/api/[id]/route.js +7 -17
- package/dist/server.d.ts +2 -0
- package/dist/server.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LotteryConfig, ClientLotteryConfig } from "../../../../features/lottery/types";
|
|
2
|
+
/** A spin prize as a public visitor may see it: what it is, not what it is worth. */
|
|
3
|
+
export interface ClientSpinPrize {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Allow-list, not a spread: `weight` and `couponId` are deliberately absent. */
|
|
9
|
+
export declare function toClientSpinPrize(prize: {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
isActive?: boolean;
|
|
13
|
+
}): ClientSpinPrize;
|
|
14
|
+
/**
|
|
15
|
+
* The three fields this projection touches; every other field passes through.
|
|
16
|
+
*
|
|
17
|
+
* ๐ `lotteryConfig` accepts EITHER shape, and that is not laxness โ it is the
|
|
18
|
+
* reason this leak survived review. `EventItem.lotteryConfig` is declared as
|
|
19
|
+
* `ClientLotteryConfig`, i.e. the already-projected type, while the repository
|
|
20
|
+
* hands back the STORED document with `price`, `weight` and `bookedByUserId`
|
|
21
|
+
* still on every slot. The type asserted the projection had happened; nothing
|
|
22
|
+
* had performed it. Accepting both means the adapter can be applied to a value
|
|
23
|
+
* whose type is lying, which is precisely where it is needed.
|
|
24
|
+
*/
|
|
25
|
+
export interface PublicEventInput {
|
|
26
|
+
createdBy?: string;
|
|
27
|
+
lotteryConfig?: LotteryConfig | ClientLotteryConfig;
|
|
28
|
+
spinPrizes?: {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
isActive?: boolean;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
/** What a caller gets back: `createdBy` gone, the two nested shapes narrowed. */
|
|
35
|
+
export type PublicEvent<T extends PublicEventInput> = Omit<T, "createdBy" | "lotteryConfig" | "spinPrizes"> & {
|
|
36
|
+
lotteryConfig?: ClientLotteryConfig;
|
|
37
|
+
spinPrizes?: ClientSpinPrize[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Project one stored event into its public shape.
|
|
41
|
+
*
|
|
42
|
+
* The return TYPE is narrowed too, not just the runtime value โ so a caller
|
|
43
|
+
* that goes looking for `slot.weight` on the result gets a compile error rather
|
|
44
|
+
* than `undefined` at runtime, and the next projection bug is caught by tsc.
|
|
45
|
+
*/
|
|
46
|
+
export declare function toPublicEvent<T extends PublicEventInput>(event: T): PublicEvent<T>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Events adapters โ the public projection for an event document.
|
|
3
|
+
*
|
|
4
|
+
* ๐ An event is published by two separate routes (appkit's own `eventIdGET`
|
|
5
|
+
* and the consumer's `/api/events` list), and both previously built their
|
|
6
|
+
* payload by SPREADING the stored document and deleting `createdBy`. A
|
|
7
|
+
* deny-list keyed on a handful of remembered field names publishes everything
|
|
8
|
+
* nobody thought to delete โ see ยง Public Data Projections and Root Cause #70.
|
|
9
|
+
*
|
|
10
|
+
* Two nested structures on `EventDocument` carry fields that decide outcomes
|
|
11
|
+
* and must never leave the server:
|
|
12
|
+
*
|
|
13
|
+
* - `lotteryConfig.slots[]` โ per-slot `price` and `weight` (the weighting IS
|
|
14
|
+
* the odds), plus the internal `bookedByUserId`.
|
|
15
|
+
* - `spinPrizes[]` โ per-prize `weight` (again, the odds) and
|
|
16
|
+
* `couponId` (an internal coupon document id).
|
|
17
|
+
*
|
|
18
|
+
* `SpinWheelView` reads `id`, `label` and `isActive` and nothing else, so
|
|
19
|
+
* dropping the other two costs the client nothing. The prize a spin actually
|
|
20
|
+
* wins is resolved server-side in `assignSpinPrize` and returned as a coupon
|
|
21
|
+
* CODE on the entry, never as the coupon's document id.
|
|
22
|
+
*
|
|
23
|
+
* Both routes call `toPublicEvent` so the two can no longer disagree
|
|
24
|
+
* (Root Cause #75 โ a rule reachable by copy-paste gets copied, and the copy
|
|
25
|
+
* never learns about the branch added later).
|
|
26
|
+
*/
|
|
27
|
+
import { toClientLotteryConfig } from "../lottery/adapters";
|
|
28
|
+
/** Allow-list, not a spread: `weight` and `couponId` are deliberately absent. */
|
|
29
|
+
export function toClientSpinPrize(prize) {
|
|
30
|
+
return {
|
|
31
|
+
id: prize.id,
|
|
32
|
+
label: prize.label,
|
|
33
|
+
isActive: prize.isActive !== false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Project one stored event into its public shape.
|
|
38
|
+
*
|
|
39
|
+
* The return TYPE is narrowed too, not just the runtime value โ so a caller
|
|
40
|
+
* that goes looking for `slot.weight` on the result gets a compile error rather
|
|
41
|
+
* than `undefined` at runtime, and the next projection bug is caught by tsc.
|
|
42
|
+
*/
|
|
43
|
+
export function toPublicEvent(event) {
|
|
44
|
+
const { createdBy: _createdBy, lotteryConfig, spinPrizes, ...rest } = event;
|
|
45
|
+
const out = rest;
|
|
46
|
+
if (lotteryConfig)
|
|
47
|
+
out.lotteryConfig = toClientLotteryConfig(lotteryConfig);
|
|
48
|
+
if (Array.isArray(spinPrizes))
|
|
49
|
+
out.spinPrizes = spinPrizes.map(toClientSpinPrize);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* before any slot/config data leaves the server.
|
|
4
4
|
*/
|
|
5
5
|
import type { LotteryConfig, LotterySlot, ClientLotteryConfig, ClientLotterySlot } from "../../../../features/lottery/types";
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Strip price and weight from a single slot for client consumption.
|
|
8
|
+
*
|
|
9
|
+
* Takes the loose shape rather than `LotterySlot` so it can be applied to a
|
|
10
|
+
* value already TYPED as projected but not actually projected โ which is the
|
|
11
|
+
* state `EventItem.lotteryConfig` is in when it comes off the repository.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toClientLotterySlot(slot: LotterySlot | ClientLotterySlot): ClientLotterySlot;
|
|
8
14
|
/** Strip price/weight from the entire config for client consumption. */
|
|
9
|
-
export declare function toClientLotteryConfig(config: LotteryConfig): ClientLotteryConfig;
|
|
15
|
+
export declare function toClientLotteryConfig(config: LotteryConfig | ClientLotteryConfig): ClientLotteryConfig;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Strip price and weight from a single slot for client consumption.
|
|
3
|
+
*
|
|
4
|
+
* Takes the loose shape rather than `LotterySlot` so it can be applied to a
|
|
5
|
+
* value already TYPED as projected but not actually projected โ which is the
|
|
6
|
+
* state `EventItem.lotteryConfig` is in when it comes off the repository.
|
|
7
|
+
*/
|
|
2
8
|
export function toClientLotterySlot(slot) {
|
|
3
9
|
// Allow-list, not a spread: every field named here is a deliberate public
|
|
4
10
|
// exposure with a known reader (ยง Public Data Projections). `image` is read
|
|
@@ -18,7 +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 {
|
|
21
|
+
import { toPublicEvent } from "../../../../_internal/server/features/events/adapters";
|
|
22
22
|
// --- GET /api/events/[id] -----------------------------------------------------
|
|
23
23
|
export async function GET(_request, context) {
|
|
24
24
|
try {
|
|
@@ -34,22 +34,12 @@ export async function GET(_request, context) {
|
|
|
34
34
|
event.status === EVENT_FIELDS.STATUS_VALUES.PAUSED) {
|
|
35
35
|
return NextResponse.json({ success: false, error: "Event not found" }, { status: 404 });
|
|
36
36
|
}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
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;
|
|
37
|
+
// ๐ Do not hand-roll this projection here. `toPublicEvent` drops
|
|
38
|
+
// `createdBy` AND allow-lists the two nested structures that decide
|
|
39
|
+
// outcomes โ `lotteryConfig.slots[]` (price / weight / bookedByUserId) and
|
|
40
|
+
// `spinPrizes[]` (weight / couponId). Both were published to anonymous
|
|
41
|
+
// callers by the deny-list spread that used to live here.
|
|
42
|
+
const publicEvent = toPublicEvent(event);
|
|
53
43
|
const entriesRepo = db.getRepository("eventEntries");
|
|
54
44
|
// For poll events with resultsVisibility='always': compute live vote tallies
|
|
55
45
|
let pollResults = [];
|
package/dist/server.d.ts
CHANGED
|
@@ -566,6 +566,8 @@ export { renderLiveItemOg, renderLiveItemOgImage, renderLiveItemOgFromDoc, type
|
|
|
566
566
|
export { getLotteryEventCached, listLotteryEvents, getLotteryEntriesForAdmin, getLotteryEntriesForUser } from "./_internal/server/features/lottery/data";
|
|
567
567
|
export type { LotteryEventClient } from "./_internal/server/features/lottery/data";
|
|
568
568
|
export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/features/lottery/adapters";
|
|
569
|
+
export { toPublicEvent, toClientSpinPrize } from "./_internal/server/features/events/adapters";
|
|
570
|
+
export type { ClientSpinPrize } from "./_internal/server/features/events/adapters";
|
|
569
571
|
export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
|
|
570
572
|
export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
|
|
571
573
|
export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
|
package/dist/server.js
CHANGED
|
@@ -1464,6 +1464,7 @@ export { renderLiveItemOg, renderLiveItemOgImage, renderLiveItemOgFromDoc } from
|
|
|
1464
1464
|
// โโ Lottery feature โ server data + adapters + actions โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1465
1465
|
export { getLotteryEventCached, listLotteryEvents, getLotteryEntriesForAdmin, getLotteryEntriesForUser } from "./_internal/server/features/lottery/data";
|
|
1466
1466
|
export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/features/lottery/adapters";
|
|
1467
|
+
export { toPublicEvent, toClientSpinPrize } from "./_internal/server/features/events/adapters";
|
|
1467
1468
|
export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
|
|
1468
1469
|
export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
|
|
1469
1470
|
export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
|