@access-dlsu/leapify 0.260605.2 → 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(),
@@ -1408,17 +1388,12 @@ classesRoute.get(
1408
1388
  registrationClosesAt: true,
1409
1389
  isSpotlight: true,
1410
1390
  maxSlots: true,
1411
- registeredSlots: true,
1412
1391
  gformsUrl: true
1413
1392
  }
1414
1393
  }),
1415
1394
  EVENTS_LIST_TTL
1416
1395
  );
1417
1396
  c.header("ETag", etag);
1418
- c.header(
1419
- "Cache-Control",
1420
- "public, max-age=604800, stale-while-revalidate=86400"
1421
- );
1422
1397
  return c.json({ data: serializeEvents(data) });
1423
1398
  }
1424
1399
  );
@@ -1442,7 +1417,8 @@ classesRoute.get(
1442
1417
  }
1443
1418
  });
1444
1419
  if (!event) throw notFound("Event");
1445
- return c.json({ data: serializeEvent(event) });
1420
+ const { registeredSlots: _, ...rest } = event;
1421
+ return c.json({ data: serializeEvent(rest) });
1446
1422
  }
1447
1423
  );
1448
1424
  classesRoute.get(
@@ -1459,11 +1435,10 @@ classesRoute.get(
1459
1435
  async (c) => {
1460
1436
  const { slug } = c.req.param();
1461
1437
  const db = createDb(c.env.DB);
1462
- const cache = new CacheService(c.env.KV);
1463
- const slotsService = new SlotsService(db, cache);
1438
+ const slotsService = new SlotsService(db);
1464
1439
  const info = await slotsService.getSlots(slug);
1465
1440
  if (!info) throw notFound("Event");
1466
- 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");
1467
1442
  return c.json({ data: info });
1468
1443
  }
1469
1444
  );
@@ -1484,9 +1459,8 @@ classesRoute.post(
1484
1459
  async (c) => {
1485
1460
  const { slug } = c.req.param();
1486
1461
  const db = createDb(c.env.DB);
1487
- const cache = new CacheService(c.env.KV);
1488
1462
  const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
1489
- const slots = new SlotsService(db, cache);
1463
+ const slots = new SlotsService(db);
1490
1464
  const event = await db.query.events.findFirst({
1491
1465
  where: eq(events.slug, slug),
1492
1466
  columns: { gformsId: true }
@@ -1828,7 +1802,7 @@ siteConfigRoute.patch(
1828
1802
  }
1829
1803
  );
1830
1804
  var FAQS_KV_KEY = "faqs:active";
1831
- var FAQS_TTL = 600;
1805
+ var FAQS_TTL = 86400;
1832
1806
  var faqSchema = z.object({
1833
1807
  question: z.string().min(1),
1834
1808
  answer: z.string().min(1),
@@ -1853,7 +1827,8 @@ faqsRoute.get(
1853
1827
  }),
1854
1828
  FAQS_TTL
1855
1829
  );
1856
- return c.json({ data });
1830
+ const serialized = data.map(({ sortOrder, ...rest }) => rest);
1831
+ return c.json({ data: serialized });
1857
1832
  }
1858
1833
  );
1859
1834
  faqsRoute.post(
@@ -1960,7 +1935,6 @@ gformsWebhookRoute.post(
1960
1935
  const { formId } = payload;
1961
1936
  if (!formId) return c.json({ error: "Missing formId" }, 400);
1962
1937
  const db = createDb(c.env.DB);
1963
- const cache = new CacheService(c.env.KV);
1964
1938
  const event = await db.query.events.findFirst({
1965
1939
  where: eq(events.gformsId, formId),
1966
1940
  columns: { slug: true, maxSlots: true, registeredSlots: true }
@@ -1969,7 +1943,7 @@ gformsWebhookRoute.post(
1969
1943
  console.warn(`[gforms-webhook] Unknown formId: ${formId}`);
1970
1944
  return c.json({ ok: true });
1971
1945
  }
1972
- const slotsService = new SlotsService(db, cache);
1946
+ const slotsService = new SlotsService(db);
1973
1947
  const updated = await slotsService.increment(event.slug);
1974
1948
  console.log(
1975
1949
  `[gforms-webhook] Incremented "${event.slug}": ${updated?.registered}/${updated?.total}`
@@ -2006,7 +1980,7 @@ async function reconcileSlots(env) {
2006
1980
  const db = createDb(env.DB);
2007
1981
  const cache = new CacheService(env.KV);
2008
1982
  const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
2009
- const slots = new SlotsService(db, cache);
1983
+ const slots = new SlotsService(db);
2010
1984
  const lock = await cache.get(LOCK_KEY);
2011
1985
  if (lock) {
2012
1986
  console.log("[reconcile-slots] Lock held, skipping.");
@@ -2240,7 +2214,7 @@ var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
2240
2214
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
2241
2215
  var uploadsRoute = new Hono();
2242
2216
  uploadsRoute.get(
2243
- "/images/*",
2217
+ "/*",
2244
2218
  describeRoute({
2245
2219
  tags: ["Uploads"],
2246
2220
  summary: "Serve an image from R2 storage",
@@ -2254,7 +2228,7 @@ uploadsRoute.get(
2254
2228
  if (!bucket) {
2255
2229
  throw serviceUnavailable("File storage (R2) is not configured.");
2256
2230
  }
2257
- const path = c.req.path.split("/uploads/images/")[1];
2231
+ const path = c.req.path.split("/uploads/")[1];
2258
2232
  if (!path) throw notFound("Image");
2259
2233
  const object = await bucket.get(path);
2260
2234
  if (!object) throw notFound("Image");
@@ -2273,7 +2247,7 @@ uploadsRoute.get(
2273
2247
  }
2274
2248
  );
2275
2249
  uploadsRoute.post(
2276
- "/images",
2250
+ "/",
2277
2251
  describeRoute({
2278
2252
  tags: ["Uploads"],
2279
2253
  summary: "Upload an image to R2 storage (admin)",
@@ -2320,7 +2294,7 @@ uploadsRoute.post(
2320
2294
  customMetadata: { uploadedAt: (/* @__PURE__ */ new Date()).toISOString() }
2321
2295
  });
2322
2296
  const url = new URL(c.req.url);
2323
- url.pathname = `${url.pathname.replace(/\/$/, "")}/${key}`;
2297
+ url.pathname = `/${key}`;
2324
2298
  url.search = "";
2325
2299
  return c.json(
2326
2300
  {
@@ -2377,7 +2351,8 @@ themesRoute.get(
2377
2351
  async (c) => {
2378
2352
  const db = createDb(c.env.DB);
2379
2353
  const data = await db.select().from(themes).orderBy(asc(themes.sortOrder), asc(themes.createdAt));
2380
- return c.json({ data });
2354
+ const serialized = data.map(({ sortOrder, ...rest }) => rest);
2355
+ return c.json({ data: serialized });
2381
2356
  }
2382
2357
  );
2383
2358
  themesRoute.post(
@@ -2624,7 +2599,7 @@ function createApp(options = {}) {
2624
2599
  documentation: {
2625
2600
  info: {
2626
2601
  title: "Leapify API",
2627
- version: "0.260605.2" ,
2602
+ version: "0.260608.1" ,
2628
2603
  description: "DLSU CSO LEAP backend API"
2629
2604
  },
2630
2605
  openapi: "3.1.0"
@@ -2742,6 +2717,7 @@ var SesError = class extends Error {
2742
2717
  this.status = status;
2743
2718
  this.name = "SesError";
2744
2719
  }
2720
+ status;
2745
2721
  /**
2746
2722
  * True for errors that are permanent (not worth retrying via SES again).
2747
2723
  * 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.