@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.
@@ -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;AAO9C,eAAO,MAAM,kBAAkB,yDAAyB,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"}
@@ -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 a local D1 counter + KV cache.
11
- * Google Forms Watch webhook increments the counter; reads go through KV.
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
- private readonly cache;
19
- constructor(db: LeapifyDb, cache: CacheService);
20
- kvKey(slug: string): string;
12
+ constructor(db: LeapifyDb);
21
13
  /**
22
- * Read current slot info KV first, D1 on miss.
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 and update KV.
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 and update KV.
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;AAEtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAI3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,OAAO,CAAA;CAChB;AAED;;;;;;GAMG;AACH,qBAAa,YAAY;IAErB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,YAAY;IAGtC,KAAK,CAAC,IAAI,EAAE,MAAM;IAIlB;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAStD;;;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;IASpE;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAqB3D;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG9C"}
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/images")) {
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, cache) {
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 KV first, D1 on miss.
980
+ * Read current slot info from D1.
982
981
  */
983
982
  async getSlots(slug) {
984
- const cached = await this.cache.get(this.kvKey(slug));
985
- if (cached) return cached;
986
- return this.refreshFromDb(slug);
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 and update KV.
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.refreshFromDb(slug);
999
+ return this.getSlots(slug);
995
1000
  }
996
1001
  /**
997
- * Atomically decrement registered_slots in D1 and update KV.
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.refreshFromDb(slug);
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 = 300;
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
- registeredSlots: true,
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
- return c.json({ data: serializeEvent(event) });
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 cache = new CacheService(c.env.KV);
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=5, stale-while-revalidate=5");
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, cache);
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 = 600;
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
- return c.json({ data });
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, cache);
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, cache);
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
- "/images/*",
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/images/")[1];
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
- "/images",
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 = `${url.pathname.replace(/\/$/, "")}/${key}`;
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
- return c.json({ data });
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.260605.1" ,
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.