@access-dlsu/leapify 0.260601.1 → 0.260601.2

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
@@ -4,7 +4,7 @@ import { Hono } from 'hono';
4
4
  import { cors } from 'hono/cors';
5
5
  import { drizzle } from 'drizzle-orm/d1';
6
6
  import { sqliteTable, integer, text, index, uniqueIndex } from 'drizzle-orm/sqlite-core';
7
- import { sql, relations, eq, and, count, lte } from 'drizzle-orm';
7
+ import { sql, relations, eq, and, count, isNotNull, lte } from 'drizzle-orm';
8
8
  import { createMiddleware } from 'hono/factory';
9
9
  import { betterAuth } from 'better-auth';
10
10
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
@@ -950,7 +950,10 @@ var GFormsService = class {
950
950
  const response = await fetch(url.toString(), {
951
951
  headers: { Authorization: `Bearer ${token}` }
952
952
  });
953
- if (!response.ok) throw new Error(`Forms API error: ${response.status}`);
953
+ if (!response.ok) {
954
+ const err = await response.text();
955
+ throw new Error(`Forms API error: ${response.status} ${err}`);
956
+ }
954
957
  const data = await response.json();
955
958
  allResponses.push(...data.responses ?? []);
956
959
  pageToken = data.nextPageToken;
@@ -1224,6 +1227,27 @@ classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
1224
1227
  c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
1225
1228
  return c.json({ data: info });
1226
1229
  });
1230
+ classesRoute.post("/:slug/reconcile", authMiddleware, adminMiddleware, async (c) => {
1231
+ const { slug } = c.req.param();
1232
+ const db = createDb(c.env.DB);
1233
+ const cache = new CacheService(c.env.KV);
1234
+ const gforms = new GFormsService(c.env.GFORMS_SERVICE_ACCOUNT_JSON);
1235
+ const slots = new SlotsService(db, cache);
1236
+ const event = await db.query.events.findFirst({
1237
+ where: eq(events.slug, slug),
1238
+ columns: { gformsId: true }
1239
+ });
1240
+ if (!event) throw notFound("Event");
1241
+ if (!event.gformsId) return c.json({ error: "No gformsId set for this event" }, 400);
1242
+ try {
1243
+ const googleCount = await gforms.getExactResponseCount(event.gformsId);
1244
+ await slots.correctCount(slug, googleCount);
1245
+ return c.json({ data: { registeredSlots: googleCount } });
1246
+ } catch (err) {
1247
+ const message = err?.message ?? "Failed to fetch from Google Forms API";
1248
+ return c.json({ error: { code: "GFORMS_API_ERROR", message } }, 502);
1249
+ }
1250
+ });
1227
1251
  classesRoute.post(
1228
1252
  "/",
1229
1253
  authMiddleware,
@@ -1531,6 +1555,196 @@ async function verifyGoogSignature(body, signature, secret) {
1531
1555
  return false;
1532
1556
  }
1533
1557
  }
1558
+ var LOCK_KEY = "cron:reconcile-slots:lock";
1559
+ var LOCK_TTL = 300;
1560
+ async function reconcileSlots(env) {
1561
+ const db = createDb(env.DB);
1562
+ const cache = new CacheService(env.KV);
1563
+ const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
1564
+ const slots = new SlotsService(db, cache);
1565
+ const lock = await cache.get(LOCK_KEY);
1566
+ if (lock) {
1567
+ console.log("[reconcile-slots] Lock held, skipping.");
1568
+ return;
1569
+ }
1570
+ await cache.set(LOCK_KEY, "1", LOCK_TTL);
1571
+ try {
1572
+ const publishedEvents = await db.query.events.findMany({
1573
+ where: isNotNull(events.gformsId),
1574
+ columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
1575
+ });
1576
+ const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
1577
+ let corrected = 0;
1578
+ for (const event of eventsWithForms) {
1579
+ try {
1580
+ const googleCount = await gforms.getExactResponseCount(event.gformsId);
1581
+ const localCount = event.registeredSlots;
1582
+ if (googleCount !== localCount) {
1583
+ console.warn(
1584
+ `[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
1585
+ );
1586
+ await slots.correctCount(event.slug, googleCount);
1587
+ corrected++;
1588
+ }
1589
+ } catch (err) {
1590
+ console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
1591
+ }
1592
+ }
1593
+ console.log(
1594
+ `[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
1595
+ );
1596
+ } finally {
1597
+ await cache.del(LOCK_KEY);
1598
+ }
1599
+ }
1600
+
1601
+ // src/routes/internal/reconcile-slots.ts
1602
+ var reconcileSlotsRoute = new Hono();
1603
+ reconcileSlotsRoute.post("/", internalMiddleware, async (c) => {
1604
+ await reconcileSlots(c.env);
1605
+ return c.json({ ok: true });
1606
+ });
1607
+ async function batchRelease(env) {
1608
+ const db = createDb(env.DB);
1609
+ const cache = new CacheService(env.KV);
1610
+ const now = Math.floor(Date.now() / 1e3);
1611
+ const toPublish = await db.query.events.findMany({
1612
+ where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
1613
+ columns: { id: true, slug: true }
1614
+ });
1615
+ if (toPublish.length === 0) return;
1616
+ const ids = toPublish.map((e) => e.id);
1617
+ await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
1618
+ // Drizzle doesn't have inArray for D1; use raw SQL for batch
1619
+ sql`${events.id} IN (${sql.join(
1620
+ ids.map((id) => sql`${id}`),
1621
+ sql`, `
1622
+ )})`
1623
+ );
1624
+ await cache.del("events:list");
1625
+ await cache.del("events:etag");
1626
+ console.log(
1627
+ `[batch-release] Published ${toPublish.length} events:`,
1628
+ toPublish.map((e) => e.slug).join(", ")
1629
+ );
1630
+ }
1631
+
1632
+ // src/routes/internal/batch-release.ts
1633
+ var batchReleaseRoute = new Hono();
1634
+ batchReleaseRoute.post("/", internalMiddleware, async (c) => {
1635
+ await batchRelease(c.env);
1636
+ return c.json({ ok: true });
1637
+ });
1638
+ function parseStartTimestamp(dateTime, startTime) {
1639
+ if (!dateTime) return null;
1640
+ const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
1641
+ const ms = Date.parse(combined);
1642
+ return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
1643
+ }
1644
+ async function reminderEmails(env) {
1645
+ if (!env.EMAIL_QUEUE) {
1646
+ console.warn(
1647
+ "[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
1648
+ );
1649
+ return;
1650
+ }
1651
+ const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
1652
+ const hasResend = !!env.RESEND_API_KEY;
1653
+ if (!hasSes && !hasResend) {
1654
+ console.warn(
1655
+ "[reminder-emails] No email providers configured. Skipping reminders."
1656
+ );
1657
+ return;
1658
+ }
1659
+ const db = createDb(env.DB);
1660
+ const now = Math.floor(Date.now() / 1e3);
1661
+ const candidates24h = await db.query.events.findMany({
1662
+ where: and(
1663
+ eq(events.status, "published"),
1664
+ eq(events.reminder24hSent, false)
1665
+ ),
1666
+ columns: {
1667
+ id: true,
1668
+ dateTime: true,
1669
+ startTime: true
1670
+ }
1671
+ });
1672
+ for (const event of candidates24h) {
1673
+ const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
1674
+ if (!startsAt) continue;
1675
+ const hoursUntil = (startsAt - now) / 3600;
1676
+ if (hoursUntil <= 25 && hoursUntil >= 23) {
1677
+ await env.EMAIL_QUEUE.send({
1678
+ type: "send_reminder_email",
1679
+ payload: { eventId: event.id, hoursBeforeEvent: 24 }
1680
+ });
1681
+ }
1682
+ }
1683
+ const candidates1h = await db.query.events.findMany({
1684
+ where: and(
1685
+ eq(events.status, "published"),
1686
+ eq(events.reminder1hSent, false)
1687
+ ),
1688
+ columns: {
1689
+ id: true,
1690
+ dateTime: true,
1691
+ startTime: true
1692
+ }
1693
+ });
1694
+ for (const event of candidates1h) {
1695
+ const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
1696
+ if (!startsAt) continue;
1697
+ const hoursUntil = (startsAt - now) / 3600;
1698
+ if (hoursUntil <= 1.5 && hoursUntil >= 0) {
1699
+ await env.EMAIL_QUEUE.send({
1700
+ type: "send_reminder_email",
1701
+ payload: { eventId: event.id, hoursBeforeEvent: 1 }
1702
+ });
1703
+ }
1704
+ }
1705
+ }
1706
+
1707
+ // src/routes/internal/reminder-emails.ts
1708
+ var reminderEmailsRoute = new Hono();
1709
+ reminderEmailsRoute.post("/", internalMiddleware, async (c) => {
1710
+ await reminderEmails(c.env);
1711
+ return c.json({ ok: true });
1712
+ });
1713
+ var RENEWAL_WINDOW = 86400;
1714
+ async function renewWatches(env) {
1715
+ const db = createDb(env.DB);
1716
+ const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
1717
+ const now = Math.floor(Date.now() / 1e3);
1718
+ const threshold = now + RENEWAL_WINDOW;
1719
+ const expiring = await db.query.events.findMany({
1720
+ where: and(
1721
+ eq(events.status, "published"),
1722
+ lte(events.watchExpiresAt, threshold)
1723
+ ),
1724
+ columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
1725
+ });
1726
+ const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
1727
+ let renewed = 0;
1728
+ for (const event of watchEvents) {
1729
+ try {
1730
+ const result = await gforms.renewWatch(event.gformsId, event.watchId);
1731
+ const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
1732
+ await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
1733
+ renewed++;
1734
+ console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
1735
+ } catch (err) {
1736
+ console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
1737
+ }
1738
+ }
1739
+ console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
1740
+ }
1741
+
1742
+ // src/routes/internal/renew-watches.ts
1743
+ var renewWatchesRoute = new Hono();
1744
+ renewWatchesRoute.post("/", internalMiddleware, async (c) => {
1745
+ await renewWatches(c.env);
1746
+ return c.json({ ok: true });
1747
+ });
1534
1748
  var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
1535
1749
  "image/jpeg",
1536
1750
  "image/png",
@@ -1797,6 +2011,10 @@ function createApp(options = {}) {
1797
2011
  app.route("/api/faqs", faqsRoute);
1798
2012
  app.route("/api/uploads", uploadsRoute);
1799
2013
  app.route("/internal/gforms-webhook", gformsWebhookRoute);
2014
+ app.route("/internal/reconcile-slots", reconcileSlotsRoute);
2015
+ app.route("/internal/batch-release", batchReleaseRoute);
2016
+ app.route("/internal/reminder-emails", reminderEmailsRoute);
2017
+ app.route("/internal/renew-watches", renewWatchesRoute);
1800
2018
  app.onError(errorHandler);
1801
2019
  app.notFound(
1802
2020
  (c) => c.json({ error: { code: "NOT_FOUND", message: "Route not found" } }, 404)
@@ -2227,168 +2445,6 @@ async function processJob(job, services) {
2227
2445
  }
2228
2446
  }
2229
2447
  }
2230
- async function batchRelease(env) {
2231
- const db = createDb(env.DB);
2232
- const cache = new CacheService(env.KV);
2233
- const now = Math.floor(Date.now() / 1e3);
2234
- const toPublish = await db.query.events.findMany({
2235
- where: and(eq(events.status, "queued"), lte(events.releaseAt, now)),
2236
- columns: { id: true, slug: true }
2237
- });
2238
- if (toPublish.length === 0) return;
2239
- const ids = toPublish.map((e) => e.id);
2240
- await db.update(events).set({ status: "published", publishedAt: sql`(unixepoch())` }).where(
2241
- // Drizzle doesn't have inArray for D1; use raw SQL for batch
2242
- sql`${events.id} IN (${sql.join(
2243
- ids.map((id) => sql`${id}`),
2244
- sql`, `
2245
- )})`
2246
- );
2247
- await cache.del("events:list");
2248
- await cache.del("events:etag");
2249
- console.log(
2250
- `[batch-release] Published ${toPublish.length} events:`,
2251
- toPublish.map((e) => e.slug).join(", ")
2252
- );
2253
- }
2254
- var LOCK_KEY = "cron:reconcile-slots:lock";
2255
- var LOCK_TTL = 300;
2256
- async function reconcileSlots(env) {
2257
- const db = createDb(env.DB);
2258
- const cache = new CacheService(env.KV);
2259
- const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
2260
- const slots = new SlotsService(db, cache);
2261
- const lock = await cache.get(LOCK_KEY);
2262
- if (lock) {
2263
- console.log("[reconcile-slots] Lock held, skipping.");
2264
- return;
2265
- }
2266
- await cache.set(LOCK_KEY, "1", LOCK_TTL);
2267
- try {
2268
- const publishedEvents = await db.query.events.findMany({
2269
- where: eq(events.status, "published"),
2270
- columns: { id: true, slug: true, gformsId: true, registeredSlots: true }
2271
- });
2272
- const eventsWithForms = publishedEvents.filter((e) => e.gformsId);
2273
- let corrected = 0;
2274
- for (const event of eventsWithForms) {
2275
- try {
2276
- const googleCount = await gforms.getExactResponseCount(event.gformsId);
2277
- const localCount = event.registeredSlots;
2278
- if (googleCount !== localCount) {
2279
- console.warn(
2280
- `[reconcile-slots] Drift on "${event.slug}": local=${localCount}, google=${googleCount}`
2281
- );
2282
- await slots.correctCount(event.slug, googleCount);
2283
- corrected++;
2284
- }
2285
- } catch (err) {
2286
- console.error(`[reconcile-slots] Error checking "${event.slug}":`, err);
2287
- }
2288
- }
2289
- console.log(
2290
- `[reconcile-slots] Checked ${eventsWithForms.length} events, corrected ${corrected}.`
2291
- );
2292
- } finally {
2293
- await cache.del(LOCK_KEY);
2294
- }
2295
- }
2296
- function parseStartTimestamp(dateTime, startTime) {
2297
- if (!dateTime) return null;
2298
- const combined = startTime ? `${dateTime} ${startTime}` : dateTime;
2299
- const ms = Date.parse(combined);
2300
- return Number.isNaN(ms) ? null : Math.floor(ms / 1e3);
2301
- }
2302
- async function reminderEmails(env) {
2303
- if (!env.EMAIL_QUEUE) {
2304
- console.warn(
2305
- "[reminder-emails] EMAIL_QUEUE binding not configured, skipping."
2306
- );
2307
- return;
2308
- }
2309
- const hasSes = !!(env.SES_REGION && env.SES_ACCESS_KEY_ID && env.SES_SECRET_ACCESS_KEY);
2310
- const hasResend = !!env.RESEND_API_KEY;
2311
- if (!hasSes && !hasResend) {
2312
- console.warn(
2313
- "[reminder-emails] No email providers configured. Skipping reminders."
2314
- );
2315
- return;
2316
- }
2317
- const db = createDb(env.DB);
2318
- const now = Math.floor(Date.now() / 1e3);
2319
- const candidates24h = await db.query.events.findMany({
2320
- where: and(
2321
- eq(events.status, "published"),
2322
- eq(events.reminder24hSent, false)
2323
- ),
2324
- columns: {
2325
- id: true,
2326
- dateTime: true,
2327
- startTime: true
2328
- }
2329
- });
2330
- for (const event of candidates24h) {
2331
- const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
2332
- if (!startsAt) continue;
2333
- const hoursUntil = (startsAt - now) / 3600;
2334
- if (hoursUntil <= 25 && hoursUntil >= 23) {
2335
- await env.EMAIL_QUEUE.send({
2336
- type: "send_reminder_email",
2337
- payload: { eventId: event.id, hoursBeforeEvent: 24 }
2338
- });
2339
- }
2340
- }
2341
- const candidates1h = await db.query.events.findMany({
2342
- where: and(
2343
- eq(events.status, "published"),
2344
- eq(events.reminder1hSent, false)
2345
- ),
2346
- columns: {
2347
- id: true,
2348
- dateTime: true,
2349
- startTime: true
2350
- }
2351
- });
2352
- for (const event of candidates1h) {
2353
- const startsAt = parseStartTimestamp(event.dateTime, event.startTime);
2354
- if (!startsAt) continue;
2355
- const hoursUntil = (startsAt - now) / 3600;
2356
- if (hoursUntil <= 1.5 && hoursUntil >= 0) {
2357
- await env.EMAIL_QUEUE.send({
2358
- type: "send_reminder_email",
2359
- payload: { eventId: event.id, hoursBeforeEvent: 1 }
2360
- });
2361
- }
2362
- }
2363
- }
2364
- var RENEWAL_WINDOW = 86400;
2365
- async function renewWatches(env) {
2366
- const db = createDb(env.DB);
2367
- const gforms = new GFormsService(env.GFORMS_SERVICE_ACCOUNT_JSON);
2368
- const now = Math.floor(Date.now() / 1e3);
2369
- const threshold = now + RENEWAL_WINDOW;
2370
- const expiring = await db.query.events.findMany({
2371
- where: and(
2372
- eq(events.status, "published"),
2373
- lte(events.watchExpiresAt, threshold)
2374
- ),
2375
- columns: { id: true, slug: true, gformsId: true, watchId: true, watchExpiresAt: true }
2376
- });
2377
- const watchEvents = expiring.filter((e) => e.gformsId && e.watchId);
2378
- let renewed = 0;
2379
- for (const event of watchEvents) {
2380
- try {
2381
- const result = await gforms.renewWatch(event.gformsId, event.watchId);
2382
- const newExpiry = Math.floor(new Date(result.expireTime).getTime() / 1e3);
2383
- await db.update(events).set({ watchExpiresAt: newExpiry }).where(eq(events.id, event.id));
2384
- renewed++;
2385
- console.log(`[renew-watches] Renewed Watch for "${event.slug}", expires ${result.expireTime}`);
2386
- } catch (err) {
2387
- console.error(`[renew-watches] Failed to renew Watch for "${event.slug}":`, err);
2388
- }
2389
- }
2390
- console.log(`[renew-watches] Renewed ${renewed}/${watchEvents.length} watches.`);
2391
- }
2392
2448
 
2393
2449
  // src/db/migrate.ts
2394
2450
  var PATCH_STATEMENTS = [
@@ -2595,7 +2651,7 @@ function defaultGetRuntimeConfig(env) {
2595
2651
  };
2596
2652
  }
2597
2653
  function injectConfig(html, config) {
2598
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2654
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
2599
2655
  return html.replace("</head>", `${configScript}</head>`);
2600
2656
  }
2601
2657
  function createWorkerHandler(options) {
@@ -2717,10 +2773,10 @@ function getRuntimeConfig(env) {
2717
2773
  };
2718
2774
  }
2719
2775
  function injectConfig2(html, config) {
2720
- const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};</script>`;
2776
+ const configScript = `<script>window.__CONFIG__=${JSON.stringify(config)};<\/script>`;
2721
2777
  return html.replace("</head>", `${configScript}</head>`);
2722
2778
  }
2723
2779
 
2724
2780
  export { authAccount, authSession, authUser, authVerification, bookmarks, bookmarksRelations, createDb, createLeapify, createQueueHandler, createWorkerHandler, ensureDatabase, events, eventsRelations, faqs, getRuntimeConfig, injectConfig2 as injectConfig, organizations, organizationsRelations, siteConfig, themes, themesRelations, users };
2725
2781
  //# sourceMappingURL=index.js.map
2726
- //# sourceMappingURL=index.js.mapap
2782
+ //# sourceMappingURL=index.js.map