@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
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
|
|
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
|
|
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
|
|
818
|
+
* Read current slot info from D1.
|
|
820
819
|
*/
|
|
821
820
|
async getSlots(slug) {
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
|
|
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
|
|
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.
|
|
837
|
+
return this.getSlots(slug);
|
|
833
838
|
}
|
|
834
839
|
/**
|
|
835
|
-
* Atomically decrement registered_slots in D1
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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/
|
|
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
|
-
"/
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|