@access-dlsu/leapify 0.260605.1 → 0.260608.1
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/index.cjs +4 -2
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +4 -2
- package/dist/client/index.js.map +1 -1
- package/dist/index.cjs +44 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +41 -66
- package/dist/index.js.map +1 -1
- package/dist/routes/internal/gforms-webhook.d.ts.map +1 -1
- package/dist/services/slots.d.ts +6 -22
- package/dist/services/slots.d.ts.map +1 -1
- package/dist/worker.js +41 -66
- package/dist/worker.js.map +1 -1
- package/package.json +10 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gforms-webhook.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/gforms-webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"gforms-webhook.d.ts","sourceRoot":"","sources":["../../../src/routes/internal/gforms-webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAM9C,eAAO,MAAM,kBAAkB,yDAAyB,CAAC"}
|
package/dist/services/slots.d.ts
CHANGED
|
@@ -1,34 +1,26 @@
|
|
|
1
1
|
import type { LeapifyDb } from '../db';
|
|
2
|
-
import type { CacheService } from './cache';
|
|
3
2
|
export interface SlotInfo {
|
|
4
|
-
available: number;
|
|
5
3
|
total: number;
|
|
6
4
|
registered: number;
|
|
7
|
-
isFull: boolean;
|
|
8
5
|
}
|
|
9
6
|
/**
|
|
10
|
-
* Manages real-time slot counts using
|
|
11
|
-
* Google Forms Watch webhook increments the counter
|
|
12
|
-
*
|
|
13
|
-
* CF Cache (Cache-Control: public, max-age=5) sits in front of the /slots
|
|
14
|
-
* endpoint, so KV is only read once per 5-second window per edge location.
|
|
7
|
+
* Manages real-time slot counts using D1 directly (no KV cache).
|
|
8
|
+
* Google Forms Watch webhook increments the counter.
|
|
15
9
|
*/
|
|
16
10
|
export declare class SlotsService {
|
|
17
11
|
private readonly db;
|
|
18
|
-
|
|
19
|
-
constructor(db: LeapifyDb, cache: CacheService);
|
|
20
|
-
kvKey(slug: string): string;
|
|
12
|
+
constructor(db: LeapifyDb);
|
|
21
13
|
/**
|
|
22
|
-
* Read current slot info
|
|
14
|
+
* Read current slot info from D1.
|
|
23
15
|
*/
|
|
24
16
|
getSlots(slug: string): Promise<SlotInfo | null>;
|
|
25
17
|
/**
|
|
26
|
-
* Atomically increment registered_slots in D1
|
|
18
|
+
* Atomically increment registered_slots in D1.
|
|
27
19
|
* Called by the Google Forms Watch webhook handler.
|
|
28
20
|
*/
|
|
29
21
|
increment(slug: string): Promise<SlotInfo | null>;
|
|
30
22
|
/**
|
|
31
|
-
* Atomically decrement registered_slots in D1
|
|
23
|
+
* Atomically decrement registered_slots in D1.
|
|
32
24
|
* Used during reconciliation drift correction (not from user actions).
|
|
33
25
|
*/
|
|
34
26
|
decrement(slug: string): Promise<SlotInfo | null>;
|
|
@@ -36,13 +28,5 @@ export declare class SlotsService {
|
|
|
36
28
|
* Set registered_slots to a specific value (used by reconciliation cron).
|
|
37
29
|
*/
|
|
38
30
|
correctCount(slug: string, actualCount: number): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* Read from D1, write to KV, and return slot info.
|
|
41
|
-
*/
|
|
42
|
-
refreshFromDb(slug: string): Promise<SlotInfo | null>;
|
|
43
|
-
/**
|
|
44
|
-
* Invalidate the KV cache for a specific event.
|
|
45
|
-
*/
|
|
46
|
-
invalidate(slug: string): Promise<void>;
|
|
47
31
|
}
|
|
48
32
|
//# sourceMappingURL=slots.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/services/slots.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/services/slots.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,SAAS;IAE1C;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IActD;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IASvD;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAWvD;;OAEG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAMrE"}
|
package/dist/worker.js
CHANGED
|
@@ -25,6 +25,8 @@ var LeapifyError = class extends Error {
|
|
|
25
25
|
this.code = code;
|
|
26
26
|
this.name = "LeapifyError";
|
|
27
27
|
}
|
|
28
|
+
statusCode;
|
|
29
|
+
code;
|
|
28
30
|
};
|
|
29
31
|
var unauthorized = (message = "Unauthorized") => new LeapifyError(401, "UNAUTHORIZED", message);
|
|
30
32
|
var domainRestricted = () => new LeapifyError(
|
|
@@ -319,7 +321,7 @@ function createCorsMiddleware(allowedOrigins) {
|
|
|
319
321
|
);
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
|
-
if (c.req.path.startsWith("/api/uploads
|
|
324
|
+
if (c.req.path.startsWith("/api/uploads")) {
|
|
323
325
|
c.header("Access-Control-Allow-Origin", "*");
|
|
324
326
|
c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
325
327
|
if (c.req.method === "OPTIONS") {
|
|
@@ -929,6 +931,7 @@ var CacheService = class {
|
|
|
929
931
|
constructor(kv) {
|
|
930
932
|
this.kv = kv;
|
|
931
933
|
}
|
|
934
|
+
kv;
|
|
932
935
|
async get(key) {
|
|
933
936
|
return this.kv.get(key, "json");
|
|
934
937
|
}
|
|
@@ -968,71 +971,48 @@ var CacheService = class {
|
|
|
968
971
|
return `"${hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")}"`;
|
|
969
972
|
}
|
|
970
973
|
};
|
|
971
|
-
var SLOT_KV_PREFIX = "slots:";
|
|
972
974
|
var SlotsService = class {
|
|
973
|
-
constructor(db
|
|
975
|
+
constructor(db) {
|
|
974
976
|
this.db = db;
|
|
975
|
-
this.cache = cache;
|
|
976
|
-
}
|
|
977
|
-
kvKey(slug) {
|
|
978
|
-
return `${SLOT_KV_PREFIX}${slug}`;
|
|
979
977
|
}
|
|
978
|
+
db;
|
|
980
979
|
/**
|
|
981
|
-
* Read current slot info
|
|
980
|
+
* Read current slot info from D1.
|
|
982
981
|
*/
|
|
983
982
|
async getSlots(slug) {
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
983
|
+
const event = await this.db.query.events.findFirst({
|
|
984
|
+
where: eq(events.slug, slug),
|
|
985
|
+
columns: { maxSlots: true, registeredSlots: true }
|
|
986
|
+
});
|
|
987
|
+
if (!event) return null;
|
|
988
|
+
return {
|
|
989
|
+
total: event.maxSlots,
|
|
990
|
+
registered: event.registeredSlots
|
|
991
|
+
};
|
|
987
992
|
}
|
|
988
993
|
/**
|
|
989
|
-
* Atomically increment registered_slots in D1
|
|
994
|
+
* Atomically increment registered_slots in D1.
|
|
990
995
|
* Called by the Google Forms Watch webhook handler.
|
|
991
996
|
*/
|
|
992
997
|
async increment(slug) {
|
|
993
998
|
await this.db.update(events).set({ registeredSlots: sql`${events.registeredSlots} + 1` }).where(eq(events.slug, slug));
|
|
994
|
-
return this.
|
|
999
|
+
return this.getSlots(slug);
|
|
995
1000
|
}
|
|
996
1001
|
/**
|
|
997
|
-
* Atomically decrement registered_slots in D1
|
|
1002
|
+
* Atomically decrement registered_slots in D1.
|
|
998
1003
|
* Used during reconciliation drift correction (not from user actions).
|
|
999
1004
|
*/
|
|
1000
1005
|
async decrement(slug) {
|
|
1001
1006
|
await this.db.update(events).set({
|
|
1002
1007
|
registeredSlots: sql`MAX(0, ${events.registeredSlots} - 1)`
|
|
1003
1008
|
}).where(eq(events.slug, slug));
|
|
1004
|
-
return this.
|
|
1009
|
+
return this.getSlots(slug);
|
|
1005
1010
|
}
|
|
1006
1011
|
/**
|
|
1007
1012
|
* Set registered_slots to a specific value (used by reconciliation cron).
|
|
1008
1013
|
*/
|
|
1009
1014
|
async correctCount(slug, actualCount) {
|
|
1010
1015
|
await this.db.update(events).set({ registeredSlots: actualCount }).where(eq(events.slug, slug));
|
|
1011
|
-
await this.invalidate(slug);
|
|
1012
|
-
}
|
|
1013
|
-
/**
|
|
1014
|
-
* Read from D1, write to KV, and return slot info.
|
|
1015
|
-
*/
|
|
1016
|
-
async refreshFromDb(slug) {
|
|
1017
|
-
const event = await this.db.query.events.findFirst({
|
|
1018
|
-
where: eq(events.slug, slug),
|
|
1019
|
-
columns: { maxSlots: true, registeredSlots: true }
|
|
1020
|
-
});
|
|
1021
|
-
if (!event) return null;
|
|
1022
|
-
const info = {
|
|
1023
|
-
total: event.maxSlots,
|
|
1024
|
-
registered: event.registeredSlots,
|
|
1025
|
-
available: Math.max(0, event.maxSlots - event.registeredSlots),
|
|
1026
|
-
isFull: event.registeredSlots >= event.maxSlots
|
|
1027
|
-
};
|
|
1028
|
-
await this.cache.set(this.kvKey(slug), info);
|
|
1029
|
-
return info;
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Invalidate the KV cache for a specific event.
|
|
1033
|
-
*/
|
|
1034
|
-
async invalidate(slug) {
|
|
1035
|
-
await this.cache.del(this.kvKey(slug));
|
|
1036
1016
|
}
|
|
1037
1017
|
};
|
|
1038
1018
|
|
|
@@ -1269,7 +1249,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
|
|
|
1269
1249
|
// src/routes/classes.ts
|
|
1270
1250
|
var EVENTS_LIST_KV_KEY = "events:list";
|
|
1271
1251
|
var EVENTS_ETAG_KV_KEY = "events:etag";
|
|
1272
|
-
var EVENTS_LIST_TTL =
|
|
1252
|
+
var EVENTS_LIST_TTL = 3600;
|
|
1273
1253
|
var createEventSchema = z.object({
|
|
1274
1254
|
themeId: z.string().min(1),
|
|
1275
1255
|
organizationId: z.string().optional(),
|
|
@@ -1397,6 +1377,7 @@ classesRoute.get(
|
|
|
1397
1377
|
themeId: true,
|
|
1398
1378
|
organizationId: true,
|
|
1399
1379
|
title: true,
|
|
1380
|
+
description: true,
|
|
1400
1381
|
venue: true,
|
|
1401
1382
|
dateTime: true,
|
|
1402
1383
|
price: true,
|
|
@@ -1407,19 +1388,12 @@ classesRoute.get(
|
|
|
1407
1388
|
registrationClosesAt: true,
|
|
1408
1389
|
isSpotlight: true,
|
|
1409
1390
|
maxSlots: true,
|
|
1410
|
-
|
|
1411
|
-
gformsUrl: true,
|
|
1412
|
-
gformsEditorUrl: true,
|
|
1413
|
-
publishedAt: true
|
|
1391
|
+
gformsUrl: true
|
|
1414
1392
|
}
|
|
1415
1393
|
}),
|
|
1416
1394
|
EVENTS_LIST_TTL
|
|
1417
1395
|
);
|
|
1418
1396
|
c.header("ETag", etag);
|
|
1419
|
-
c.header(
|
|
1420
|
-
"Cache-Control",
|
|
1421
|
-
"public, max-age=604800, stale-while-revalidate=86400"
|
|
1422
|
-
);
|
|
1423
1397
|
return c.json({ data: serializeEvents(data) });
|
|
1424
1398
|
}
|
|
1425
1399
|
);
|
|
@@ -1443,7 +1417,8 @@ classesRoute.get(
|
|
|
1443
1417
|
}
|
|
1444
1418
|
});
|
|
1445
1419
|
if (!event) throw notFound("Event");
|
|
1446
|
-
|
|
1420
|
+
const { registeredSlots: _, ...rest } = event;
|
|
1421
|
+
return c.json({ data: serializeEvent(rest) });
|
|
1447
1422
|
}
|
|
1448
1423
|
);
|
|
1449
1424
|
classesRoute.get(
|
|
@@ -1460,11 +1435,10 @@ classesRoute.get(
|
|
|
1460
1435
|
async (c) => {
|
|
1461
1436
|
const { slug } = c.req.param();
|
|
1462
1437
|
const db = createDb(c.env.DB);
|
|
1463
|
-
const
|
|
1464
|
-
const slotsService = new SlotsService(db, cache);
|
|
1438
|
+
const slotsService = new SlotsService(db);
|
|
1465
1439
|
const info = await slotsService.getSlots(slug);
|
|
1466
1440
|
if (!info) throw notFound("Event");
|
|
1467
|
-
c.header("Cache-Control", "public, max-age=
|
|
1441
|
+
c.header("Cache-Control", "public, max-age=3, stale-while-revalidate=3");
|
|
1468
1442
|
return c.json({ data: info });
|
|
1469
1443
|
}
|
|
1470
1444
|
);
|
|
@@ -1485,9 +1459,8 @@ classesRoute.post(
|
|
|
1485
1459
|
async (c) => {
|
|
1486
1460
|
const { slug } = c.req.param();
|
|
1487
1461
|
const db = createDb(c.env.DB);
|
|
1488
|
-
const cache = new CacheService(c.env.KV);
|
|
1489
1462
|
const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
1490
|
-
const slots = new SlotsService(db
|
|
1463
|
+
const slots = new SlotsService(db);
|
|
1491
1464
|
const event = await db.query.events.findFirst({
|
|
1492
1465
|
where: eq(events.slug, slug),
|
|
1493
1466
|
columns: { gformsId: true }
|
|
@@ -1829,7 +1802,7 @@ siteConfigRoute.patch(
|
|
|
1829
1802
|
}
|
|
1830
1803
|
);
|
|
1831
1804
|
var FAQS_KV_KEY = "faqs:active";
|
|
1832
|
-
var FAQS_TTL =
|
|
1805
|
+
var FAQS_TTL = 86400;
|
|
1833
1806
|
var faqSchema = z.object({
|
|
1834
1807
|
question: z.string().min(1),
|
|
1835
1808
|
answer: z.string().min(1),
|
|
@@ -1854,7 +1827,8 @@ faqsRoute.get(
|
|
|
1854
1827
|
}),
|
|
1855
1828
|
FAQS_TTL
|
|
1856
1829
|
);
|
|
1857
|
-
|
|
1830
|
+
const serialized = data.map(({ sortOrder, ...rest }) => rest);
|
|
1831
|
+
return c.json({ data: serialized });
|
|
1858
1832
|
}
|
|
1859
1833
|
);
|
|
1860
1834
|
faqsRoute.post(
|
|
@@ -1961,7 +1935,6 @@ gformsWebhookRoute.post(
|
|
|
1961
1935
|
const { formId } = payload;
|
|
1962
1936
|
if (!formId) return c.json({ error: "Missing formId" }, 400);
|
|
1963
1937
|
const db = createDb(c.env.DB);
|
|
1964
|
-
const cache = new CacheService(c.env.KV);
|
|
1965
1938
|
const event = await db.query.events.findFirst({
|
|
1966
1939
|
where: eq(events.gformsId, formId),
|
|
1967
1940
|
columns: { slug: true, maxSlots: true, registeredSlots: true }
|
|
@@ -1970,7 +1943,7 @@ gformsWebhookRoute.post(
|
|
|
1970
1943
|
console.warn(`[gforms-webhook] Unknown formId: ${formId}`);
|
|
1971
1944
|
return c.json({ ok: true });
|
|
1972
1945
|
}
|
|
1973
|
-
const slotsService = new SlotsService(db
|
|
1946
|
+
const slotsService = new SlotsService(db);
|
|
1974
1947
|
const updated = await slotsService.increment(event.slug);
|
|
1975
1948
|
console.log(
|
|
1976
1949
|
`[gforms-webhook] Incremented "${event.slug}": ${updated?.registered}/${updated?.total}`
|
|
@@ -2007,7 +1980,7 @@ async function reconcileSlots(env) {
|
|
|
2007
1980
|
const db = createDb(env.DB);
|
|
2008
1981
|
const cache = new CacheService(env.KV);
|
|
2009
1982
|
const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
|
|
2010
|
-
const slots = new SlotsService(db
|
|
1983
|
+
const slots = new SlotsService(db);
|
|
2011
1984
|
const lock = await cache.get(LOCK_KEY);
|
|
2012
1985
|
if (lock) {
|
|
2013
1986
|
console.log("[reconcile-slots] Lock held, skipping.");
|
|
@@ -2241,7 +2214,7 @@ var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
|
2241
2214
|
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
2242
2215
|
var uploadsRoute = new Hono();
|
|
2243
2216
|
uploadsRoute.get(
|
|
2244
|
-
"
|
|
2217
|
+
"/*",
|
|
2245
2218
|
describeRoute({
|
|
2246
2219
|
tags: ["Uploads"],
|
|
2247
2220
|
summary: "Serve an image from R2 storage",
|
|
@@ -2255,7 +2228,7 @@ uploadsRoute.get(
|
|
|
2255
2228
|
if (!bucket) {
|
|
2256
2229
|
throw serviceUnavailable("File storage (R2) is not configured.");
|
|
2257
2230
|
}
|
|
2258
|
-
const path = c.req.path.split("/uploads/
|
|
2231
|
+
const path = c.req.path.split("/uploads/")[1];
|
|
2259
2232
|
if (!path) throw notFound("Image");
|
|
2260
2233
|
const object = await bucket.get(path);
|
|
2261
2234
|
if (!object) throw notFound("Image");
|
|
@@ -2274,7 +2247,7 @@ uploadsRoute.get(
|
|
|
2274
2247
|
}
|
|
2275
2248
|
);
|
|
2276
2249
|
uploadsRoute.post(
|
|
2277
|
-
"/
|
|
2250
|
+
"/",
|
|
2278
2251
|
describeRoute({
|
|
2279
2252
|
tags: ["Uploads"],
|
|
2280
2253
|
summary: "Upload an image to R2 storage (admin)",
|
|
@@ -2321,7 +2294,7 @@ uploadsRoute.post(
|
|
|
2321
2294
|
customMetadata: { uploadedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2322
2295
|
});
|
|
2323
2296
|
const url = new URL(c.req.url);
|
|
2324
|
-
url.pathname =
|
|
2297
|
+
url.pathname = `/${key}`;
|
|
2325
2298
|
url.search = "";
|
|
2326
2299
|
return c.json(
|
|
2327
2300
|
{
|
|
@@ -2378,7 +2351,8 @@ themesRoute.get(
|
|
|
2378
2351
|
async (c) => {
|
|
2379
2352
|
const db = createDb(c.env.DB);
|
|
2380
2353
|
const data = await db.select().from(themes).orderBy(asc(themes.sortOrder), asc(themes.createdAt));
|
|
2381
|
-
|
|
2354
|
+
const serialized = data.map(({ sortOrder, ...rest }) => rest);
|
|
2355
|
+
return c.json({ data: serialized });
|
|
2382
2356
|
}
|
|
2383
2357
|
);
|
|
2384
2358
|
themesRoute.post(
|
|
@@ -2625,7 +2599,7 @@ function createApp(options = {}) {
|
|
|
2625
2599
|
documentation: {
|
|
2626
2600
|
info: {
|
|
2627
2601
|
title: "Leapify API",
|
|
2628
|
-
version: "0.
|
|
2602
|
+
version: "0.260608.1" ,
|
|
2629
2603
|
description: "DLSU CSO LEAP backend API"
|
|
2630
2604
|
},
|
|
2631
2605
|
openapi: "3.1.0"
|
|
@@ -2743,6 +2717,7 @@ var SesError = class extends Error {
|
|
|
2743
2717
|
this.status = status;
|
|
2744
2718
|
this.name = "SesError";
|
|
2745
2719
|
}
|
|
2720
|
+
status;
|
|
2746
2721
|
/**
|
|
2747
2722
|
* True for errors that are permanent (not worth retrying via SES again).
|
|
2748
2723
|
* 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.
|