@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/index.js CHANGED
@@ -21,6 +21,8 @@ var LeapifyError = class extends Error {
21
21
  this.code = code;
22
22
  this.name = "LeapifyError";
23
23
  }
24
+ statusCode;
25
+ code;
24
26
  };
25
27
  var unauthorized = (message = "Unauthorized") => new LeapifyError(401, "UNAUTHORIZED", message);
26
28
  var domainRestricted = () => new LeapifyError(
@@ -315,7 +317,7 @@ function createCorsMiddleware(allowedOrigins) {
315
317
  );
316
318
  }
317
319
  }
318
- if (c.req.path.startsWith("/api/uploads/images")) {
320
+ if (c.req.path.startsWith("/api/uploads")) {
319
321
  c.header("Access-Control-Allow-Origin", "*");
320
322
  c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
321
323
  if (c.req.method === "OPTIONS") {
@@ -767,6 +769,7 @@ var CacheService = class {
767
769
  constructor(kv) {
768
770
  this.kv = kv;
769
771
  }
772
+ kv;
770
773
  async get(key) {
771
774
  return this.kv.get(key, "json");
772
775
  }
@@ -806,71 +809,48 @@ var CacheService = class {
806
809
  return `"${hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")}"`;
807
810
  }
808
811
  };
809
- var SLOT_KV_PREFIX = "slots:";
810
812
  var SlotsService = class {
811
- constructor(db, cache) {
813
+ constructor(db) {
812
814
  this.db = db;
813
- this.cache = cache;
814
- }
815
- kvKey(slug) {
816
- return `${SLOT_KV_PREFIX}${slug}`;
817
815
  }
816
+ db;
818
817
  /**
819
- * Read current slot info KV first, D1 on miss.
818
+ * Read current slot info from D1.
820
819
  */
821
820
  async getSlots(slug) {
822
- const cached = await this.cache.get(this.kvKey(slug));
823
- if (cached) return cached;
824
- return this.refreshFromDb(slug);
821
+ const event = await this.db.query.events.findFirst({
822
+ where: eq(events.slug, slug),
823
+ columns: { maxSlots: true, registeredSlots: true }
824
+ });
825
+ if (!event) return null;
826
+ return {
827
+ total: event.maxSlots,
828
+ registered: event.registeredSlots
829
+ };
825
830
  }
826
831
  /**
827
- * Atomically increment registered_slots in D1 and update KV.
832
+ * Atomically increment registered_slots in D1.
828
833
  * Called by the Google Forms Watch webhook handler.
829
834
  */
830
835
  async increment(slug) {
831
836
  await this.db.update(events).set({ registeredSlots: sql`${events.registeredSlots} + 1` }).where(eq(events.slug, slug));
832
- return this.refreshFromDb(slug);
837
+ return this.getSlots(slug);
833
838
  }
834
839
  /**
835
- * Atomically decrement registered_slots in D1 and update KV.
840
+ * Atomically decrement registered_slots in D1.
836
841
  * Used during reconciliation drift correction (not from user actions).
837
842
  */
838
843
  async decrement(slug) {
839
844
  await this.db.update(events).set({
840
845
  registeredSlots: sql`MAX(0, ${events.registeredSlots} - 1)`
841
846
  }).where(eq(events.slug, slug));
842
- return this.refreshFromDb(slug);
847
+ return this.getSlots(slug);
843
848
  }
844
849
  /**
845
850
  * Set registered_slots to a specific value (used by reconciliation cron).
846
851
  */
847
852
  async correctCount(slug, actualCount) {
848
853
  await this.db.update(events).set({ registeredSlots: actualCount }).where(eq(events.slug, slug));
849
- await this.invalidate(slug);
850
- }
851
- /**
852
- * Read from D1, write to KV, and return slot info.
853
- */
854
- async refreshFromDb(slug) {
855
- const event = await this.db.query.events.findFirst({
856
- where: eq(events.slug, slug),
857
- columns: { maxSlots: true, registeredSlots: true }
858
- });
859
- if (!event) return null;
860
- const info = {
861
- total: event.maxSlots,
862
- registered: event.registeredSlots,
863
- available: Math.max(0, event.maxSlots - event.registeredSlots),
864
- isFull: event.registeredSlots >= event.maxSlots
865
- };
866
- await this.cache.set(this.kvKey(slug), info);
867
- return info;
868
- }
869
- /**
870
- * Invalidate the KV cache for a specific event.
871
- */
872
- async invalidate(slug) {
873
- await this.cache.del(this.kvKey(slug));
874
854
  }
875
855
  };
876
856
 
@@ -1107,7 +1087,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
1107
1087
  // src/routes/classes.ts
1108
1088
  var EVENTS_LIST_KV_KEY = "events:list";
1109
1089
  var EVENTS_ETAG_KV_KEY = "events:etag";
1110
- var EVENTS_LIST_TTL = 300;
1090
+ var EVENTS_LIST_TTL = 3600;
1111
1091
  var createEventSchema = z.object({
1112
1092
  themeId: z.string().min(1),
1113
1093
  organizationId: z.string().optional(),
@@ -1235,6 +1215,7 @@ classesRoute.get(
1235
1215
  themeId: true,
1236
1216
  organizationId: true,
1237
1217
  title: true,
1218
+ description: true,
1238
1219
  venue: true,
1239
1220
  dateTime: true,
1240
1221
  price: true,
@@ -1245,19 +1226,12 @@ classesRoute.get(
1245
1226
  registrationClosesAt: true,
1246
1227
  isSpotlight: true,
1247
1228
  maxSlots: true,
1248
- registeredSlots: true,
1249
- gformsUrl: true,
1250
- gformsEditorUrl: true,
1251
- publishedAt: true
1229
+ gformsUrl: true
1252
1230
  }
1253
1231
  }),
1254
1232
  EVENTS_LIST_TTL
1255
1233
  );
1256
1234
  c.header("ETag", etag);
1257
- c.header(
1258
- "Cache-Control",
1259
- "public, max-age=604800, stale-while-revalidate=86400"
1260
- );
1261
1235
  return c.json({ data: serializeEvents(data) });
1262
1236
  }
1263
1237
  );
@@ -1281,7 +1255,8 @@ classesRoute.get(
1281
1255
  }
1282
1256
  });
1283
1257
  if (!event) throw notFound("Event");
1284
- return c.json({ data: serializeEvent(event) });
1258
+ const { registeredSlots: _, ...rest } = event;
1259
+ return c.json({ data: serializeEvent(rest) });
1285
1260
  }
1286
1261
  );
1287
1262
  classesRoute.get(
@@ -1298,11 +1273,10 @@ classesRoute.get(
1298
1273
  async (c) => {
1299
1274
  const { slug } = c.req.param();
1300
1275
  const db = createDb(c.env.DB);
1301
- const cache = new CacheService(c.env.KV);
1302
- const slotsService = new SlotsService(db, cache);
1276
+ const slotsService = new SlotsService(db);
1303
1277
  const info = await slotsService.getSlots(slug);
1304
1278
  if (!info) throw notFound("Event");
1305
- c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
1279
+ c.header("Cache-Control", "public, max-age=3, stale-while-revalidate=3");
1306
1280
  return c.json({ data: info });
1307
1281
  }
1308
1282
  );
@@ -1323,9 +1297,8 @@ classesRoute.post(
1323
1297
  async (c) => {
1324
1298
  const { slug } = c.req.param();
1325
1299
  const db = createDb(c.env.DB);
1326
- const cache = new CacheService(c.env.KV);
1327
1300
  const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
1328
- const slots = new SlotsService(db, cache);
1301
+ const slots = new SlotsService(db);
1329
1302
  const event = await db.query.events.findFirst({
1330
1303
  where: eq(events.slug, slug),
1331
1304
  columns: { gformsId: true }
@@ -1667,7 +1640,7 @@ siteConfigRoute.patch(
1667
1640
  }
1668
1641
  );
1669
1642
  var FAQS_KV_KEY = "faqs:active";
1670
- var FAQS_TTL = 600;
1643
+ var FAQS_TTL = 86400;
1671
1644
  var faqSchema = z.object({
1672
1645
  question: z.string().min(1),
1673
1646
  answer: z.string().min(1),
@@ -1692,7 +1665,8 @@ faqsRoute.get(
1692
1665
  }),
1693
1666
  FAQS_TTL
1694
1667
  );
1695
- return c.json({ data });
1668
+ const serialized = data.map(({ sortOrder, ...rest }) => rest);
1669
+ return c.json({ data: serialized });
1696
1670
  }
1697
1671
  );
1698
1672
  faqsRoute.post(
@@ -1799,7 +1773,6 @@ gformsWebhookRoute.post(
1799
1773
  const { formId } = payload;
1800
1774
  if (!formId) return c.json({ error: "Missing formId" }, 400);
1801
1775
  const db = createDb(c.env.DB);
1802
- const cache = new CacheService(c.env.KV);
1803
1776
  const event = await db.query.events.findFirst({
1804
1777
  where: eq(events.gformsId, formId),
1805
1778
  columns: { slug: true, maxSlots: true, registeredSlots: true }
@@ -1808,7 +1781,7 @@ gformsWebhookRoute.post(
1808
1781
  console.warn(`[gforms-webhook] Unknown formId: ${formId}`);
1809
1782
  return c.json({ ok: true });
1810
1783
  }
1811
- const slotsService = new SlotsService(db, cache);
1784
+ const slotsService = new SlotsService(db);
1812
1785
  const updated = await slotsService.increment(event.slug);
1813
1786
  console.log(
1814
1787
  `[gforms-webhook] Incremented "${event.slug}": ${updated?.registered}/${updated?.total}`
@@ -1845,7 +1818,7 @@ async function reconcileSlots(env) {
1845
1818
  const db = createDb(env.DB);
1846
1819
  const cache = new CacheService(env.KV);
1847
1820
  const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
1848
- const slots = new SlotsService(db, cache);
1821
+ const slots = new SlotsService(db);
1849
1822
  const lock = await cache.get(LOCK_KEY);
1850
1823
  if (lock) {
1851
1824
  console.log("[reconcile-slots] Lock held, skipping.");
@@ -2079,7 +2052,7 @@ var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
2079
2052
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
2080
2053
  var uploadsRoute = new Hono();
2081
2054
  uploadsRoute.get(
2082
- "/images/*",
2055
+ "/*",
2083
2056
  describeRoute({
2084
2057
  tags: ["Uploads"],
2085
2058
  summary: "Serve an image from R2 storage",
@@ -2093,7 +2066,7 @@ uploadsRoute.get(
2093
2066
  if (!bucket) {
2094
2067
  throw serviceUnavailable("File storage (R2) is not configured.");
2095
2068
  }
2096
- const path = c.req.path.split("/uploads/images/")[1];
2069
+ const path = c.req.path.split("/uploads/")[1];
2097
2070
  if (!path) throw notFound("Image");
2098
2071
  const object = await bucket.get(path);
2099
2072
  if (!object) throw notFound("Image");
@@ -2112,7 +2085,7 @@ uploadsRoute.get(
2112
2085
  }
2113
2086
  );
2114
2087
  uploadsRoute.post(
2115
- "/images",
2088
+ "/",
2116
2089
  describeRoute({
2117
2090
  tags: ["Uploads"],
2118
2091
  summary: "Upload an image to R2 storage (admin)",
@@ -2159,7 +2132,7 @@ uploadsRoute.post(
2159
2132
  customMetadata: { uploadedAt: (/* @__PURE__ */ new Date()).toISOString() }
2160
2133
  });
2161
2134
  const url = new URL(c.req.url);
2162
- url.pathname = `${url.pathname.replace(/\/$/, "")}/${key}`;
2135
+ url.pathname = `/${key}`;
2163
2136
  url.search = "";
2164
2137
  return c.json(
2165
2138
  {
@@ -2216,7 +2189,8 @@ themesRoute.get(
2216
2189
  async (c) => {
2217
2190
  const db = createDb(c.env.DB);
2218
2191
  const data = await db.select().from(themes).orderBy(asc(themes.sortOrder), asc(themes.createdAt));
2219
- return c.json({ data });
2192
+ const serialized = data.map(({ sortOrder, ...rest }) => rest);
2193
+ return c.json({ data: serialized });
2220
2194
  }
2221
2195
  );
2222
2196
  themesRoute.post(
@@ -2463,7 +2437,7 @@ function createApp(options = {}) {
2463
2437
  documentation: {
2464
2438
  info: {
2465
2439
  title: "Leapify API",
2466
- version: "0.260605.1" ,
2440
+ version: "0.260608.1" ,
2467
2441
  description: "DLSU CSO LEAP backend API"
2468
2442
  },
2469
2443
  openapi: "3.1.0"
@@ -2581,6 +2555,7 @@ var SesError = class extends Error {
2581
2555
  this.status = status;
2582
2556
  this.name = "SesError";
2583
2557
  }
2558
+ status;
2584
2559
  /**
2585
2560
  * True for errors that are permanent (not worth retrying via SES again).
2586
2561
  * 400 BadRequest, 403 Forbidden, 404 NotFound → non-retryable.