@access-dlsu/leapify 0.260507.1 → 0.260507.4
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/app.d.ts.map +1 -1
- package/dist/{chunk-QARF2YFF.cjs → chunk-BFMJDSDI.cjs} +3 -2
- package/dist/chunk-BFMJDSDI.cjs.map +1 -0
- package/dist/{chunk-ANNHE3PZ.js → chunk-LJ5BSSYE.js} +3 -2
- package/dist/{chunk-QARF2YFF.cjs.map → chunk-LJ5BSSYE.js.map} +1 -1
- package/dist/{chunk-63CUZGSZ.js → chunk-MCOLCTFX.js} +3 -2
- package/dist/chunk-MCOLCTFX.js.map +1 -0
- package/dist/{chunk-YFJBE3AU.cjs → chunk-MKWVLWVJ.cjs} +3 -2
- package/dist/chunk-MKWVLWVJ.cjs.map +1 -0
- package/dist/client/index.cjs +25 -25
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.ts +17 -17
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +25 -25
- package/dist/client/index.js.map +1 -1
- package/dist/client/types.d.ts +4 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/schema/{events.d.ts → classes.d.ts} +3 -3
- package/dist/db/schema/classes.d.ts.map +1 -0
- package/dist/db/schema/index.d.ts +1 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/site-config.d.ts +83 -0
- package/dist/db/schema/site-config.d.ts.map +1 -1
- package/dist/index.cjs +640 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +616 -33
- package/dist/index.js.map +1 -1
- package/dist/lib/middleware/pow-challenge.cjs +6 -6
- package/dist/lib/middleware/pow-challenge.d.ts.map +1 -1
- package/dist/lib/middleware/pow-challenge.js +1 -1
- package/dist/routes/classes.d.ts +4 -0
- package/dist/routes/classes.d.ts.map +1 -0
- package/dist/routes/contentful-sync.d.ts +4 -0
- package/dist/routes/contentful-sync.d.ts.map +1 -0
- package/dist/services/snapshot.d.ts +1 -1
- package/dist/services/snapshot.d.ts.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker-handler.d.ts.map +1 -1
- package/dist/worker.js +605 -29
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-63CUZGSZ.js.map +0 -1
- package/dist/chunk-ANNHE3PZ.js.map +0 -1
- package/dist/chunk-YFJBE3AU.cjs.map +0 -1
- package/dist/db/schema/events.d.ts.map +0 -1
- package/dist/routes/events.d.ts +0 -4
- package/dist/routes/events.d.ts.map +0 -1
package/dist/worker.js
CHANGED
|
@@ -25089,6 +25089,12 @@ var errorHandler2 = (err, c) => {
|
|
|
25089
25089
|
);
|
|
25090
25090
|
};
|
|
25091
25091
|
|
|
25092
|
+
// src/types.ts
|
|
25093
|
+
function parseCmsMode(raw2) {
|
|
25094
|
+
if (raw2 === "cloudflare" || raw2 === "contentful") return raw2;
|
|
25095
|
+
return "hybrid";
|
|
25096
|
+
}
|
|
25097
|
+
|
|
25092
25098
|
// node_modules/hono/dist/middleware/cors/index.js
|
|
25093
25099
|
var cors = (options) => {
|
|
25094
25100
|
const opts = {
|
|
@@ -25399,6 +25405,7 @@ function createPowChallengeMiddleware() {
|
|
|
25399
25405
|
return createMiddleware(async (c, next) => {
|
|
25400
25406
|
if (c.req.path === POW_VERIFY_PATH) return next();
|
|
25401
25407
|
if (EXEMPT_PATHS.some((p) => c.req.path.startsWith(p))) return next();
|
|
25408
|
+
if (c.req.method === "OPTIONS") return next();
|
|
25402
25409
|
if (c.req.header("Authorization")) return next();
|
|
25403
25410
|
const cookieHeader = c.req.header("Cookie") ?? "";
|
|
25404
25411
|
const cookieMatch = cookieHeader.match(
|
|
@@ -62511,12 +62518,14 @@ function drizzle(client, config3 = {}) {
|
|
|
62511
62518
|
// src/db/schema/index.ts
|
|
62512
62519
|
var schema_exports = {};
|
|
62513
62520
|
__export(schema_exports, {
|
|
62521
|
+
CONTENTFUL_CONFIG_KEYS: () => CONTENTFUL_CONFIG_KEYS,
|
|
62514
62522
|
authAccount: () => authAccount,
|
|
62515
62523
|
authSession: () => authSession,
|
|
62516
62524
|
authUser: () => authUser,
|
|
62517
62525
|
authVerification: () => authVerification,
|
|
62518
62526
|
bookmarks: () => bookmarks,
|
|
62519
62527
|
bookmarksRelations: () => bookmarksRelations,
|
|
62528
|
+
contentfulConfig: () => contentfulConfig,
|
|
62520
62529
|
events: () => events,
|
|
62521
62530
|
eventsRelations: () => eventsRelations,
|
|
62522
62531
|
faqs: () => faqs,
|
|
@@ -62568,7 +62577,7 @@ var organizationsRelations = relations(organizations, ({ many }) => ({
|
|
|
62568
62577
|
events: many(events)
|
|
62569
62578
|
}));
|
|
62570
62579
|
|
|
62571
|
-
// src/db/schema/
|
|
62580
|
+
// src/db/schema/classes.ts
|
|
62572
62581
|
var events = sqliteTable(
|
|
62573
62582
|
"events",
|
|
62574
62583
|
{
|
|
@@ -62593,7 +62602,7 @@ var events = sqliteTable(
|
|
|
62593
62602
|
// start time string
|
|
62594
62603
|
endTime: text("end_time"),
|
|
62595
62604
|
// end time string
|
|
62596
|
-
|
|
62605
|
+
isSpotlight: integer2("is_spotlight", { mode: "boolean" }).notNull().default(false),
|
|
62597
62606
|
// Slot tracking (local counter — NOT polled from Google Forms)
|
|
62598
62607
|
maxSlots: integer2("max_slots").notNull().default(0),
|
|
62599
62608
|
registeredSlots: integer2("registered_slots").notNull().default(0),
|
|
@@ -62650,6 +62659,18 @@ var siteConfig = sqliteTable("site_config", {
|
|
|
62650
62659
|
// JSON-serializable string
|
|
62651
62660
|
updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
|
|
62652
62661
|
});
|
|
62662
|
+
var CONTENTFUL_CONFIG_KEYS = {
|
|
62663
|
+
ENABLED: "contentful.enabled",
|
|
62664
|
+
SPACE_ID: "contentful.spaceId",
|
|
62665
|
+
MANAGEMENT_TOKEN: "contentful.managementToken",
|
|
62666
|
+
DEFAULT_SPACE_ID: "dlsu-events"
|
|
62667
|
+
};
|
|
62668
|
+
var contentfulConfig = sqliteTable("contentful_config", {
|
|
62669
|
+
space_id: text("space_id"),
|
|
62670
|
+
contentful_enabled: integer2("contentful_enabled").notNull().default(0),
|
|
62671
|
+
last_sync_at: integer2("last_sync_at"),
|
|
62672
|
+
updated_at: integer2("updated_at").default(sql2`(unixepoch())`).notNull()
|
|
62673
|
+
});
|
|
62653
62674
|
|
|
62654
62675
|
// src/db/schema/faqs.ts
|
|
62655
62676
|
var faqs = sqliteTable("faqs", {
|
|
@@ -63819,7 +63840,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
|
|
|
63819
63840
|
identifier: "uid"
|
|
63820
63841
|
});
|
|
63821
63842
|
|
|
63822
|
-
// src/routes/
|
|
63843
|
+
// src/routes/classes.ts
|
|
63823
63844
|
var EVENTS_LIST_KV_KEY = "events:list";
|
|
63824
63845
|
var EVENTS_ETAG_KV_KEY = "events:etag";
|
|
63825
63846
|
var EVENTS_LIST_TTL = 300;
|
|
@@ -63835,7 +63856,7 @@ async function pushEventToContentful(env2, event) {
|
|
|
63835
63856
|
const fields = {
|
|
63836
63857
|
title: ContentfulManagement.locale(event.title),
|
|
63837
63858
|
slug: ContentfulManagement.locale(event.slug),
|
|
63838
|
-
|
|
63859
|
+
isSpotlight: ContentfulManagement.locale(event.isSpotlight),
|
|
63839
63860
|
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
63840
63861
|
};
|
|
63841
63862
|
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
@@ -63898,7 +63919,7 @@ var createEventSchema = external_exports.object({
|
|
|
63898
63919
|
startTime: external_exports.string().optional(),
|
|
63899
63920
|
endTime: external_exports.string().optional(),
|
|
63900
63921
|
registrationClosesAt: external_exports.number().optional(),
|
|
63901
|
-
|
|
63922
|
+
isSpotlight: external_exports.boolean().default(false),
|
|
63902
63923
|
maxSlots: external_exports.number().int().min(0).default(0),
|
|
63903
63924
|
gformsId: external_exports.string().optional(),
|
|
63904
63925
|
gformsUrl: external_exports.string().url().optional(),
|
|
@@ -63907,11 +63928,11 @@ var createEventSchema = external_exports.object({
|
|
|
63907
63928
|
contentfulEntryId: external_exports.string().optional(),
|
|
63908
63929
|
status: external_exports.enum(["draft", "queued", "published"]).default("draft")
|
|
63909
63930
|
});
|
|
63910
|
-
var
|
|
63931
|
+
var classesRoute = new Hono2();
|
|
63911
63932
|
function generateSlug(title) {
|
|
63912
63933
|
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
63913
63934
|
}
|
|
63914
|
-
|
|
63935
|
+
classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
|
|
63915
63936
|
const db = createDb(c.env.DB);
|
|
63916
63937
|
const data = await db.query.events.findMany({
|
|
63917
63938
|
with: { theme: true, organization: true },
|
|
@@ -63919,7 +63940,7 @@ eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
|
|
|
63919
63940
|
});
|
|
63920
63941
|
return c.json({ data });
|
|
63921
63942
|
});
|
|
63922
|
-
|
|
63943
|
+
classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
|
|
63923
63944
|
const body = await c.req.json();
|
|
63924
63945
|
const db = createDb(c.env.DB);
|
|
63925
63946
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -63947,7 +63968,7 @@ eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) =>
|
|
|
63947
63968
|
]);
|
|
63948
63969
|
return c.json({ data: { updated: body.ids.length } });
|
|
63949
63970
|
});
|
|
63950
|
-
|
|
63971
|
+
classesRoute.get("/", eventsListRateLimit, async (c) => {
|
|
63951
63972
|
const db = createDb(c.env.DB);
|
|
63952
63973
|
const cache3 = new CacheService(c.env.KV);
|
|
63953
63974
|
const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
|
|
@@ -63958,7 +63979,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
63958
63979
|
);
|
|
63959
63980
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
63960
63981
|
if (ifNoneMatch === etag) {
|
|
63961
|
-
return c.
|
|
63982
|
+
return c.body(null, 304);
|
|
63962
63983
|
}
|
|
63963
63984
|
const data = await cache3.getOrSet(
|
|
63964
63985
|
EVENTS_LIST_KV_KEY,
|
|
@@ -63982,7 +64003,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
63982
64003
|
startTime: true,
|
|
63983
64004
|
endTime: true,
|
|
63984
64005
|
registrationClosesAt: true,
|
|
63985
|
-
|
|
64006
|
+
isSpotlight: true,
|
|
63986
64007
|
maxSlots: true,
|
|
63987
64008
|
registeredSlots: true,
|
|
63988
64009
|
gformsUrl: true,
|
|
@@ -63999,7 +64020,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
63999
64020
|
);
|
|
64000
64021
|
return c.json({ data });
|
|
64001
64022
|
});
|
|
64002
|
-
|
|
64023
|
+
classesRoute.get("/:slug", async (c) => {
|
|
64003
64024
|
const { slug } = c.req.param();
|
|
64004
64025
|
const db = createDb(c.env.DB);
|
|
64005
64026
|
const event = await db.query.events.findFirst({
|
|
@@ -64011,7 +64032,7 @@ eventsRoute.get("/:slug", async (c) => {
|
|
|
64011
64032
|
if (!event) throw notFound("Event");
|
|
64012
64033
|
return c.json({ data: event });
|
|
64013
64034
|
});
|
|
64014
|
-
|
|
64035
|
+
classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
64015
64036
|
const { slug } = c.req.param();
|
|
64016
64037
|
const db = createDb(c.env.DB);
|
|
64017
64038
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -64021,7 +64042,7 @@ eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
|
64021
64042
|
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
64022
64043
|
return c.json({ data: info2 });
|
|
64023
64044
|
});
|
|
64024
|
-
|
|
64045
|
+
classesRoute.post(
|
|
64025
64046
|
"/",
|
|
64026
64047
|
authMiddleware,
|
|
64027
64048
|
adminMiddleware,
|
|
@@ -64056,11 +64077,13 @@ eventsRoute.post(
|
|
|
64056
64077
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
64057
64078
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
64058
64079
|
]);
|
|
64059
|
-
c.
|
|
64080
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
64081
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
|
|
64082
|
+
}
|
|
64060
64083
|
return c.json({ data: created }, 201);
|
|
64061
64084
|
}
|
|
64062
64085
|
);
|
|
64063
|
-
|
|
64086
|
+
classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
64064
64087
|
const { slug } = c.req.param();
|
|
64065
64088
|
const body = await c.req.json();
|
|
64066
64089
|
const db = createDb(c.env.DB);
|
|
@@ -64075,10 +64098,12 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64075
64098
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
64076
64099
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
64077
64100
|
]);
|
|
64078
|
-
c.
|
|
64101
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
64102
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
|
|
64103
|
+
}
|
|
64079
64104
|
return c.json({ data: updated });
|
|
64080
64105
|
});
|
|
64081
|
-
|
|
64106
|
+
classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
64082
64107
|
const { slug } = c.req.param();
|
|
64083
64108
|
const db = createDb(c.env.DB);
|
|
64084
64109
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -64177,7 +64202,447 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
|
|
|
64177
64202
|
return c.json({ data: { bookmarked: false } });
|
|
64178
64203
|
});
|
|
64179
64204
|
|
|
64205
|
+
// src/services/contentful.ts
|
|
64206
|
+
var CONTENTFUL_CDN = "https://cdn.contentful.com";
|
|
64207
|
+
var ContentfulService = class _ContentfulService {
|
|
64208
|
+
spaceId;
|
|
64209
|
+
accessToken;
|
|
64210
|
+
environment;
|
|
64211
|
+
constructor(spaceId, accessToken, environment = "master") {
|
|
64212
|
+
this.spaceId = spaceId;
|
|
64213
|
+
this.accessToken = accessToken;
|
|
64214
|
+
this.environment = environment;
|
|
64215
|
+
}
|
|
64216
|
+
/**
|
|
64217
|
+
* Returns true if the required Contentful credentials are configured.
|
|
64218
|
+
*/
|
|
64219
|
+
static isConfigured(spaceId, accessToken) {
|
|
64220
|
+
return !!(spaceId && accessToken);
|
|
64221
|
+
}
|
|
64222
|
+
// ─── Entries ─────────────────────────────────────────────────────────────
|
|
64223
|
+
/**
|
|
64224
|
+
* Fetch all entries of a given content type.
|
|
64225
|
+
* Handles pagination automatically (100 per page, Contentful max).
|
|
64226
|
+
*/
|
|
64227
|
+
async getEntries(contentTypeId) {
|
|
64228
|
+
const allItems = [];
|
|
64229
|
+
let skip = 0;
|
|
64230
|
+
const limit = 100;
|
|
64231
|
+
do {
|
|
64232
|
+
const url2 = this.buildUrl(`/entries`, {
|
|
64233
|
+
content_type: contentTypeId,
|
|
64234
|
+
skip: String(skip),
|
|
64235
|
+
limit: String(limit),
|
|
64236
|
+
include: "2"
|
|
64237
|
+
// resolve up to 2 levels of linked entries/assets
|
|
64238
|
+
});
|
|
64239
|
+
const res = await fetch(url2, { headers: this.headers() });
|
|
64240
|
+
if (!res.ok) {
|
|
64241
|
+
throw new Error(`Contentful entries error: ${res.status} ${await res.text()}`);
|
|
64242
|
+
}
|
|
64243
|
+
const data = await res.json();
|
|
64244
|
+
allItems.push(...data.items);
|
|
64245
|
+
skip += limit;
|
|
64246
|
+
if (allItems.length >= data.total) break;
|
|
64247
|
+
} while (true);
|
|
64248
|
+
return allItems;
|
|
64249
|
+
}
|
|
64250
|
+
/**
|
|
64251
|
+
* Fetch all assets. Handles pagination.
|
|
64252
|
+
*/
|
|
64253
|
+
async getAssets() {
|
|
64254
|
+
const allItems = [];
|
|
64255
|
+
let skip = 0;
|
|
64256
|
+
const limit = 100;
|
|
64257
|
+
do {
|
|
64258
|
+
const url2 = this.buildUrl(`/assets`, {
|
|
64259
|
+
skip: String(skip),
|
|
64260
|
+
limit: String(limit)
|
|
64261
|
+
});
|
|
64262
|
+
const res = await fetch(url2, { headers: this.headers() });
|
|
64263
|
+
if (!res.ok) {
|
|
64264
|
+
throw new Error(`Contentful assets error: ${res.status} ${await res.text()}`);
|
|
64265
|
+
}
|
|
64266
|
+
const data = await res.json();
|
|
64267
|
+
allItems.push(...data.items);
|
|
64268
|
+
skip += limit;
|
|
64269
|
+
if (allItems.length >= data.total) break;
|
|
64270
|
+
} while (true);
|
|
64271
|
+
return allItems;
|
|
64272
|
+
}
|
|
64273
|
+
// ─── Asset file download ─────────────────────────────────────────────────
|
|
64274
|
+
/**
|
|
64275
|
+
* Download an asset file from Contentful's CDN.
|
|
64276
|
+
* Returns the raw ArrayBuffer and content type.
|
|
64277
|
+
*/
|
|
64278
|
+
async downloadAsset(assetUrl) {
|
|
64279
|
+
const url2 = assetUrl.startsWith("//") ? `https:${assetUrl}` : assetUrl;
|
|
64280
|
+
const res = await fetch(url2);
|
|
64281
|
+
if (!res.ok) {
|
|
64282
|
+
throw new Error(`Failed to download asset: ${res.status}`);
|
|
64283
|
+
}
|
|
64284
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
64285
|
+
const data = await res.arrayBuffer();
|
|
64286
|
+
return { data, contentType };
|
|
64287
|
+
}
|
|
64288
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
64289
|
+
/**
|
|
64290
|
+
* Extract a field value from a Contentful entry, handling locale wrapping.
|
|
64291
|
+
* Contentful fields are often `{ "en-US": value }` — this unwraps them.
|
|
64292
|
+
*/
|
|
64293
|
+
static getField(entry, fieldName) {
|
|
64294
|
+
const raw2 = entry.fields[fieldName];
|
|
64295
|
+
if (raw2 === void 0 || raw2 === null) return void 0;
|
|
64296
|
+
if (typeof raw2 === "object" && !Array.isArray(raw2) && "en-US" in raw2) {
|
|
64297
|
+
return raw2["en-US"];
|
|
64298
|
+
}
|
|
64299
|
+
return raw2;
|
|
64300
|
+
}
|
|
64301
|
+
/**
|
|
64302
|
+
* Extract a linked entry/sys reference ID from a reference field.
|
|
64303
|
+
*/
|
|
64304
|
+
static getRefId(entry, fieldName) {
|
|
64305
|
+
const ref = _ContentfulService.getField(entry, fieldName);
|
|
64306
|
+
return ref?.sys?.id;
|
|
64307
|
+
}
|
|
64308
|
+
/**
|
|
64309
|
+
* Extract an asset URL from a linked asset field.
|
|
64310
|
+
*/
|
|
64311
|
+
static getAssetUrl(entry, fieldName) {
|
|
64312
|
+
const asset = _ContentfulService.getField(entry, fieldName);
|
|
64313
|
+
return asset?.sys?.id;
|
|
64314
|
+
}
|
|
64315
|
+
/**
|
|
64316
|
+
* Resolve an asset URL by ID from a list of fetched assets.
|
|
64317
|
+
*/
|
|
64318
|
+
static resolveAssetUrl(assets, assetId) {
|
|
64319
|
+
const asset = assets.find((a) => a.sys.id === assetId);
|
|
64320
|
+
return asset?.fields?.file?.url;
|
|
64321
|
+
}
|
|
64322
|
+
// ─── Private ─────────────────────────────────────────────────────────────
|
|
64323
|
+
headers() {
|
|
64324
|
+
return {
|
|
64325
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
64326
|
+
"Content-Type": "application/json"
|
|
64327
|
+
};
|
|
64328
|
+
}
|
|
64329
|
+
buildUrl(path, params = {}) {
|
|
64330
|
+
const url2 = new URL(
|
|
64331
|
+
`/spaces/${this.spaceId}/environments/${this.environment}${path}`,
|
|
64332
|
+
CONTENTFUL_CDN
|
|
64333
|
+
);
|
|
64334
|
+
for (const [key, value] of Object.entries(params)) {
|
|
64335
|
+
url2.searchParams.set(key, value);
|
|
64336
|
+
}
|
|
64337
|
+
return url2.toString();
|
|
64338
|
+
}
|
|
64339
|
+
};
|
|
64340
|
+
|
|
64180
64341
|
// src/services/snapshot.ts
|
|
64342
|
+
var DEFAULT_FIELDS = {
|
|
64343
|
+
event: {
|
|
64344
|
+
title: "title",
|
|
64345
|
+
slug: "slug",
|
|
64346
|
+
theme: "theme",
|
|
64347
|
+
organization: "organization",
|
|
64348
|
+
venue: "venue",
|
|
64349
|
+
date: "date",
|
|
64350
|
+
startTime: "startTime",
|
|
64351
|
+
endTime: "endTime",
|
|
64352
|
+
price: "price",
|
|
64353
|
+
image: "image",
|
|
64354
|
+
isSpotlight: "isSpotlight",
|
|
64355
|
+
maxSlots: "maxSlots",
|
|
64356
|
+
gformsUrl: "gformsUrl",
|
|
64357
|
+
gformsEditorUrl: "gformsEditorUrl",
|
|
64358
|
+
registrationClosesAt: "registrationClosesAt",
|
|
64359
|
+
classCode: "classCode"
|
|
64360
|
+
},
|
|
64361
|
+
theme: {
|
|
64362
|
+
name: "name",
|
|
64363
|
+
path: "path"
|
|
64364
|
+
},
|
|
64365
|
+
faq: {
|
|
64366
|
+
question: "question",
|
|
64367
|
+
answer: "answer",
|
|
64368
|
+
category: "category",
|
|
64369
|
+
sortOrder: "sortOrder"
|
|
64370
|
+
},
|
|
64371
|
+
organization: {
|
|
64372
|
+
name: "name",
|
|
64373
|
+
acronym: "acronym",
|
|
64374
|
+
logoUrl: "logoUrl",
|
|
64375
|
+
link: "link"
|
|
64376
|
+
},
|
|
64377
|
+
siteConfig: {
|
|
64378
|
+
key: "key",
|
|
64379
|
+
value: "value"
|
|
64380
|
+
}
|
|
64381
|
+
};
|
|
64382
|
+
var CONTENTFUL_CACHE_PREFIX = "contentful:cache";
|
|
64383
|
+
var CONTENTFUL_CACHE_TTL = 300;
|
|
64384
|
+
async function snapshotAllContent(db, bucket, contentful, config3 = {}, kv) {
|
|
64385
|
+
const mergedConfig = {
|
|
64386
|
+
eventTypeId: config3.eventTypeId ?? "event",
|
|
64387
|
+
themeTypeId: config3.themeTypeId ?? "theme",
|
|
64388
|
+
faqTypeId: config3.faqTypeId ?? "faq",
|
|
64389
|
+
organizationTypeId: config3.organizationTypeId ?? "organization",
|
|
64390
|
+
fields: {
|
|
64391
|
+
event: { ...DEFAULT_FIELDS.event, ...config3.fields?.event },
|
|
64392
|
+
theme: { ...DEFAULT_FIELDS.theme, ...config3.fields?.theme },
|
|
64393
|
+
faq: { ...DEFAULT_FIELDS.faq, ...config3.fields?.faq },
|
|
64394
|
+
organization: { ...DEFAULT_FIELDS.organization, ...config3.fields?.organization },
|
|
64395
|
+
siteConfig: { ...DEFAULT_FIELDS.siteConfig, ...config3.fields?.siteConfig }
|
|
64396
|
+
}
|
|
64397
|
+
};
|
|
64398
|
+
const result = {
|
|
64399
|
+
themesSynced: 0,
|
|
64400
|
+
eventsSynced: 0,
|
|
64401
|
+
faqsSynced: 0,
|
|
64402
|
+
organizationsSynced: 0,
|
|
64403
|
+
imagesUploaded: 0,
|
|
64404
|
+
imagesSkipped: 0,
|
|
64405
|
+
errors: []
|
|
64406
|
+
};
|
|
64407
|
+
let allAssets = [];
|
|
64408
|
+
if (bucket) {
|
|
64409
|
+
try {
|
|
64410
|
+
allAssets = await contentful.getAssets();
|
|
64411
|
+
} catch (err) {
|
|
64412
|
+
result.errors.push(`Failed to fetch assets: ${err}`);
|
|
64413
|
+
}
|
|
64414
|
+
}
|
|
64415
|
+
try {
|
|
64416
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:themes`;
|
|
64417
|
+
let themeEntries;
|
|
64418
|
+
if (kv) {
|
|
64419
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64420
|
+
if (cached2) {
|
|
64421
|
+
themeEntries = cached2;
|
|
64422
|
+
} else {
|
|
64423
|
+
themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
|
|
64424
|
+
await kv.put(cacheKey, JSON.stringify(themeEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64425
|
+
}
|
|
64426
|
+
} else {
|
|
64427
|
+
themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
|
|
64428
|
+
}
|
|
64429
|
+
result.themesSynced = await syncThemes(db, themeEntries, mergedConfig);
|
|
64430
|
+
} catch (err) {
|
|
64431
|
+
result.errors.push(`Themes sync failed: ${err}`);
|
|
64432
|
+
}
|
|
64433
|
+
try {
|
|
64434
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:organizations`;
|
|
64435
|
+
let orgEntries;
|
|
64436
|
+
if (kv) {
|
|
64437
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64438
|
+
if (cached2) {
|
|
64439
|
+
orgEntries = cached2;
|
|
64440
|
+
} else {
|
|
64441
|
+
orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
|
|
64442
|
+
await kv.put(cacheKey, JSON.stringify(orgEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64443
|
+
}
|
|
64444
|
+
} else {
|
|
64445
|
+
orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
|
|
64446
|
+
}
|
|
64447
|
+
await syncOrganizations(db, orgEntries, mergedConfig);
|
|
64448
|
+
} catch (err) {
|
|
64449
|
+
result.errors.push(`Organizations sync failed: ${err}`);
|
|
64450
|
+
}
|
|
64451
|
+
try {
|
|
64452
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:events`;
|
|
64453
|
+
let eventEntries;
|
|
64454
|
+
if (kv) {
|
|
64455
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64456
|
+
if (cached2) {
|
|
64457
|
+
eventEntries = cached2;
|
|
64458
|
+
} else {
|
|
64459
|
+
eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
|
|
64460
|
+
await kv.put(cacheKey, JSON.stringify(eventEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64461
|
+
}
|
|
64462
|
+
} else {
|
|
64463
|
+
eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
|
|
64464
|
+
}
|
|
64465
|
+
result.eventsSynced = await syncEvents(db, bucket, contentful, allAssets, eventEntries, mergedConfig, result);
|
|
64466
|
+
} catch (err) {
|
|
64467
|
+
result.errors.push(`Events sync failed: ${err}`);
|
|
64468
|
+
}
|
|
64469
|
+
try {
|
|
64470
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:faqs`;
|
|
64471
|
+
let faqEntries;
|
|
64472
|
+
if (kv) {
|
|
64473
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64474
|
+
if (cached2) {
|
|
64475
|
+
faqEntries = cached2;
|
|
64476
|
+
} else {
|
|
64477
|
+
faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
|
|
64478
|
+
await kv.put(cacheKey, JSON.stringify(faqEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64479
|
+
}
|
|
64480
|
+
} else {
|
|
64481
|
+
faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
|
|
64482
|
+
}
|
|
64483
|
+
result.faqsSynced = await syncFaqs(db, faqEntries, mergedConfig);
|
|
64484
|
+
} catch (err) {
|
|
64485
|
+
result.errors.push(`FAQs sync failed: ${err}`);
|
|
64486
|
+
}
|
|
64487
|
+
console.log(
|
|
64488
|
+
`[Snapshot] Complete: ${result.themesSynced} themes, ${result.organizationsSynced} organizations, ${result.eventsSynced} events, ${result.faqsSynced} FAQs, ${result.imagesUploaded} images uploaded, ${result.imagesSkipped} images skipped, ${result.errors.length} errors`
|
|
64489
|
+
);
|
|
64490
|
+
return result;
|
|
64491
|
+
}
|
|
64492
|
+
async function syncThemes(db, entries, config3) {
|
|
64493
|
+
let count2 = 0;
|
|
64494
|
+
for (const entry of entries) {
|
|
64495
|
+
const cfId = entry.sys.id;
|
|
64496
|
+
const name = ContentfulService.getField(entry, config3.fields.theme.name);
|
|
64497
|
+
const path = ContentfulService.getField(entry, config3.fields.theme.path);
|
|
64498
|
+
if (!name || !path) continue;
|
|
64499
|
+
await db.insert(themes).values({ id: cfId, name, path }).onConflictDoUpdate({
|
|
64500
|
+
target: themes.id,
|
|
64501
|
+
set: { name, path }
|
|
64502
|
+
});
|
|
64503
|
+
count2++;
|
|
64504
|
+
}
|
|
64505
|
+
return count2;
|
|
64506
|
+
}
|
|
64507
|
+
async function syncOrganizations(db, entries, config3) {
|
|
64508
|
+
let count2 = 0;
|
|
64509
|
+
for (const entry of entries) {
|
|
64510
|
+
const cfId = entry.sys.id;
|
|
64511
|
+
const f = config3.fields.organization;
|
|
64512
|
+
const name = ContentfulService.getField(entry, f.name);
|
|
64513
|
+
const acronym = ContentfulService.getField(entry, f.acronym);
|
|
64514
|
+
if (!name || !acronym) continue;
|
|
64515
|
+
const logoUrl = ContentfulService.getField(entry, f.logoUrl) ?? null;
|
|
64516
|
+
const link = ContentfulService.getField(entry, f.link) ?? null;
|
|
64517
|
+
await db.insert(organizations).values({ id: cfId, name, acronym, logoUrl, link }).onConflictDoUpdate({
|
|
64518
|
+
target: organizations.id,
|
|
64519
|
+
set: { name, acronym, logoUrl, link }
|
|
64520
|
+
});
|
|
64521
|
+
count2++;
|
|
64522
|
+
}
|
|
64523
|
+
return count2;
|
|
64524
|
+
}
|
|
64525
|
+
async function syncEvents(db, bucket, contentful, allAssets, entries, config3, result) {
|
|
64526
|
+
let count2 = 0;
|
|
64527
|
+
for (const entry of entries) {
|
|
64528
|
+
try {
|
|
64529
|
+
const cfId = entry.sys.id;
|
|
64530
|
+
const f = config3.fields.event;
|
|
64531
|
+
const title = ContentfulService.getField(entry, f.title);
|
|
64532
|
+
if (!title) {
|
|
64533
|
+
result.errors.push(`Event ${cfId}: missing title, skipping`);
|
|
64534
|
+
continue;
|
|
64535
|
+
}
|
|
64536
|
+
const slug = ContentfulService.getField(entry, f.slug) ?? slugify2(title);
|
|
64537
|
+
const themeRef = ContentfulService.getField(entry, f.theme);
|
|
64538
|
+
const themeId = themeRef?.sys?.id ?? null;
|
|
64539
|
+
const orgRef = ContentfulService.getField(entry, f.organization);
|
|
64540
|
+
const organizationId = orgRef?.sys?.id ?? null;
|
|
64541
|
+
let backgroundImageUrl = null;
|
|
64542
|
+
if (bucket) {
|
|
64543
|
+
const imageRef = ContentfulService.getField(entry, f.image);
|
|
64544
|
+
const assetId = imageRef?.sys?.id;
|
|
64545
|
+
if (assetId) {
|
|
64546
|
+
const assetUrl = ContentfulService.resolveAssetUrl(allAssets, assetId);
|
|
64547
|
+
if (assetUrl) {
|
|
64548
|
+
const r2Key = `contentful/${assetId}`;
|
|
64549
|
+
const uploaded = await uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key);
|
|
64550
|
+
if (uploaded.skipped) {
|
|
64551
|
+
result.imagesSkipped++;
|
|
64552
|
+
} else {
|
|
64553
|
+
result.imagesUploaded++;
|
|
64554
|
+
}
|
|
64555
|
+
backgroundImageUrl = `/uploads/images/${r2Key}`;
|
|
64556
|
+
}
|
|
64557
|
+
}
|
|
64558
|
+
}
|
|
64559
|
+
const values = {
|
|
64560
|
+
id: cfId,
|
|
64561
|
+
contentfulEntryId: cfId,
|
|
64562
|
+
title,
|
|
64563
|
+
slug,
|
|
64564
|
+
themeId,
|
|
64565
|
+
organizationId,
|
|
64566
|
+
updatedAt: entry.sys.updatedAt
|
|
64567
|
+
};
|
|
64568
|
+
const venue = ContentfulService.getField(entry, f.venue);
|
|
64569
|
+
if (venue !== void 0) values.venue = venue;
|
|
64570
|
+
const date5 = ContentfulService.getField(entry, f.date);
|
|
64571
|
+
if (date5 !== void 0) values.dateTime = date5;
|
|
64572
|
+
const price = ContentfulService.getField(entry, f.price);
|
|
64573
|
+
if (price !== void 0) values.price = price;
|
|
64574
|
+
if (backgroundImageUrl) values.backgroundImageUrl = backgroundImageUrl;
|
|
64575
|
+
const isSpotlight = ContentfulService.getField(entry, f.isSpotlight);
|
|
64576
|
+
if (isSpotlight !== void 0) values.isSpotlight = isSpotlight;
|
|
64577
|
+
const maxSlots = ContentfulService.getField(entry, f.maxSlots);
|
|
64578
|
+
if (maxSlots !== void 0) values.maxSlots = maxSlots;
|
|
64579
|
+
const gformsUrl = ContentfulService.getField(entry, f.gformsUrl);
|
|
64580
|
+
if (gformsUrl !== void 0) values.gformsUrl = gformsUrl;
|
|
64581
|
+
const gformsEditorUrl = ContentfulService.getField(entry, f.gformsEditorUrl);
|
|
64582
|
+
if (gformsEditorUrl !== void 0) values.gformsEditorUrl = gformsEditorUrl;
|
|
64583
|
+
const classCode = ContentfulService.getField(entry, f.classCode);
|
|
64584
|
+
if (classCode !== void 0) values.classCode = classCode;
|
|
64585
|
+
const startTime = ContentfulService.getField(entry, f.startTime);
|
|
64586
|
+
if (startTime !== void 0) values.startTime = startTime;
|
|
64587
|
+
const endTime = ContentfulService.getField(entry, f.endTime);
|
|
64588
|
+
if (endTime !== void 0) values.endTime = endTime;
|
|
64589
|
+
const regCloseRaw = ContentfulService.getField(entry, f.registrationClosesAt);
|
|
64590
|
+
if (regCloseRaw) {
|
|
64591
|
+
const ms = Date.parse(regCloseRaw);
|
|
64592
|
+
if (!Number.isNaN(ms)) values.registrationClosesAt = Math.floor(ms / 1e3);
|
|
64593
|
+
}
|
|
64594
|
+
await db.insert(events).values(values).onConflictDoUpdate({
|
|
64595
|
+
target: events.id,
|
|
64596
|
+
set: values
|
|
64597
|
+
});
|
|
64598
|
+
count2++;
|
|
64599
|
+
} catch (err) {
|
|
64600
|
+
result.errors.push(`Event ${entry.sys.id}: ${err}`);
|
|
64601
|
+
}
|
|
64602
|
+
}
|
|
64603
|
+
return count2;
|
|
64604
|
+
}
|
|
64605
|
+
async function syncFaqs(db, entries, config3) {
|
|
64606
|
+
let count2 = 0;
|
|
64607
|
+
for (const entry of entries) {
|
|
64608
|
+
const cfId = entry.sys.id;
|
|
64609
|
+
const f = config3.fields.faq;
|
|
64610
|
+
const question = ContentfulService.getField(entry, f.question);
|
|
64611
|
+
const answer = ContentfulService.getField(entry, f.answer);
|
|
64612
|
+
if (!question || !answer) continue;
|
|
64613
|
+
const category = ContentfulService.getField(entry, f.category) ?? null;
|
|
64614
|
+
const sortOrder = ContentfulService.getField(entry, f.sortOrder) ?? 0;
|
|
64615
|
+
await db.insert(faqs).values({
|
|
64616
|
+
id: cfId,
|
|
64617
|
+
question,
|
|
64618
|
+
answer,
|
|
64619
|
+
category,
|
|
64620
|
+
sortOrder
|
|
64621
|
+
}).onConflictDoUpdate({
|
|
64622
|
+
target: faqs.id,
|
|
64623
|
+
set: { question, answer, category, sortOrder }
|
|
64624
|
+
});
|
|
64625
|
+
count2++;
|
|
64626
|
+
}
|
|
64627
|
+
return count2;
|
|
64628
|
+
}
|
|
64629
|
+
async function uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key) {
|
|
64630
|
+
const { data, contentType } = await contentful.downloadAsset(assetUrl);
|
|
64631
|
+
const sha = await computeSha256(data);
|
|
64632
|
+
const existing = await bucket.head(r2Key);
|
|
64633
|
+
if (existing?.customMetadata?.sha256 === sha) {
|
|
64634
|
+
return { skipped: true };
|
|
64635
|
+
}
|
|
64636
|
+
await bucket.put(r2Key, data, {
|
|
64637
|
+
httpMetadata: { contentType },
|
|
64638
|
+
customMetadata: {
|
|
64639
|
+
sha256: sha,
|
|
64640
|
+
source: "contentful",
|
|
64641
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
64642
|
+
}
|
|
64643
|
+
});
|
|
64644
|
+
return { skipped: false };
|
|
64645
|
+
}
|
|
64181
64646
|
async function batchRun(items, fn, concurrency = 5) {
|
|
64182
64647
|
const results = [];
|
|
64183
64648
|
for (let i = 0; i < items.length; i += concurrency) {
|
|
@@ -64187,6 +64652,13 @@ async function batchRun(items, fn, concurrency = 5) {
|
|
|
64187
64652
|
}
|
|
64188
64653
|
return results;
|
|
64189
64654
|
}
|
|
64655
|
+
async function computeSha256(data) {
|
|
64656
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
64657
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
64658
|
+
}
|
|
64659
|
+
function slugify2(text2) {
|
|
64660
|
+
return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
64661
|
+
}
|
|
64190
64662
|
async function ensureContentTypes(mgmt, config3 = {}) {
|
|
64191
64663
|
const eventTypeId = config3.eventTypeId ?? "event";
|
|
64192
64664
|
const themeTypeId = config3.themeTypeId ?? "theme";
|
|
@@ -64207,7 +64679,7 @@ async function ensureContentTypes(mgmt, config3 = {}) {
|
|
|
64207
64679
|
{ id: "endTime", name: "End Time", type: "Symbol" },
|
|
64208
64680
|
{ id: "price", name: "Price", type: "Symbol" },
|
|
64209
64681
|
{ id: "image", name: "Image", type: "Link", linkType: "Asset" },
|
|
64210
|
-
{ id: "
|
|
64682
|
+
{ id: "isSpotlight", name: "Spotlight", type: "Boolean" },
|
|
64211
64683
|
{ id: "maxSlots", name: "Max Slots", type: "Integer" },
|
|
64212
64684
|
{ id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
|
|
64213
64685
|
{ id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
|
|
@@ -64292,7 +64764,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
|
|
|
64292
64764
|
const fields = {
|
|
64293
64765
|
title: ContentfulManagement.locale(event.title),
|
|
64294
64766
|
slug: ContentfulManagement.locale(event.slug),
|
|
64295
|
-
|
|
64767
|
+
isSpotlight: ContentfulManagement.locale(event.isSpotlight),
|
|
64296
64768
|
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
64297
64769
|
};
|
|
64298
64770
|
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
@@ -64363,6 +64835,9 @@ siteConfigRoute.get("/", async (c) => {
|
|
|
64363
64835
|
siteName: config3.site_name ?? null,
|
|
64364
64836
|
registrationGloballyOpen: config3.registration_globally_open ?? true,
|
|
64365
64837
|
maintenanceMode: config3.maintenance_mode ?? false,
|
|
64838
|
+
// Prefer the D1-persisted value over the env-var default so that
|
|
64839
|
+
// PATCH /config/cms_mode changes are reflected immediately.
|
|
64840
|
+
cmsMode: config3.cms_mode ?? c.get("cmsMode"),
|
|
64366
64841
|
now: Math.floor(Date.now() / 1e3)
|
|
64367
64842
|
}
|
|
64368
64843
|
});
|
|
@@ -64384,6 +64859,10 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64384
64859
|
var SYNC_LOCK_KEY = "contentful:sync:lock";
|
|
64385
64860
|
var SYNC_LOCK_TTL = 60;
|
|
64386
64861
|
siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
|
|
64862
|
+
const cmsMode = c.get("cmsMode");
|
|
64863
|
+
if (cmsMode === "cloudflare") {
|
|
64864
|
+
throw serviceUnavailable("Contentful sync is not available in Cloudflare-only mode.");
|
|
64865
|
+
}
|
|
64387
64866
|
if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
|
|
64388
64867
|
throw serviceUnavailable("Contentful Management API credentials not configured.");
|
|
64389
64868
|
}
|
|
@@ -64470,7 +64949,9 @@ faqsRoute.post(
|
|
|
64470
64949
|
const cache3 = new CacheService(c.env.KV);
|
|
64471
64950
|
const [created] = await db.insert(faqs).values(body).returning();
|
|
64472
64951
|
await cache3.del(FAQS_KV_KEY);
|
|
64473
|
-
c.
|
|
64952
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
64953
|
+
c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
|
|
64954
|
+
}
|
|
64474
64955
|
return c.json({ data: created }, 201);
|
|
64475
64956
|
}
|
|
64476
64957
|
);
|
|
@@ -64483,7 +64964,9 @@ faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64483
64964
|
const [updated] = await db.update(faqs).set({ ...body, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
|
|
64484
64965
|
if (!updated) throw notFound("FAQ");
|
|
64485
64966
|
await cache3.del(FAQS_KV_KEY);
|
|
64486
|
-
c.
|
|
64967
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
64968
|
+
c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
|
|
64969
|
+
}
|
|
64487
64970
|
return c.json({ data: updated });
|
|
64488
64971
|
});
|
|
64489
64972
|
faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
@@ -64759,7 +65242,9 @@ themesRoute.post(
|
|
|
64759
65242
|
const db = createDb(c.env.DB);
|
|
64760
65243
|
try {
|
|
64761
65244
|
const [created] = await db.insert(themes).values(body).returning();
|
|
64762
|
-
c.
|
|
65245
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65246
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
|
|
65247
|
+
}
|
|
64763
65248
|
return c.json({ data: created }, 201);
|
|
64764
65249
|
} catch (err) {
|
|
64765
65250
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -64780,7 +65265,9 @@ themesRoute.patch(
|
|
|
64780
65265
|
try {
|
|
64781
65266
|
const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
|
|
64782
65267
|
if (!updated) throw notFound("Theme");
|
|
64783
|
-
c.
|
|
65268
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65269
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
|
|
65270
|
+
}
|
|
64784
65271
|
return c.json({ data: updated });
|
|
64785
65272
|
} catch (err) {
|
|
64786
65273
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -64795,7 +65282,9 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64795
65282
|
const db = createDb(c.env.DB);
|
|
64796
65283
|
const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
|
|
64797
65284
|
if (!deleted) throw notFound("Theme");
|
|
64798
|
-
c.
|
|
65285
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65286
|
+
c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
|
|
65287
|
+
}
|
|
64799
65288
|
return c.body(null, 204);
|
|
64800
65289
|
});
|
|
64801
65290
|
|
|
@@ -64859,6 +65348,83 @@ organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) =>
|
|
|
64859
65348
|
return c.body(null, 204);
|
|
64860
65349
|
});
|
|
64861
65350
|
|
|
65351
|
+
// src/routes/contentful-sync.ts
|
|
65352
|
+
var contentfulSyncRoute = new Hono2();
|
|
65353
|
+
contentfulSyncRoute.post(
|
|
65354
|
+
"/trigger",
|
|
65355
|
+
authMiddleware,
|
|
65356
|
+
adminMiddleware,
|
|
65357
|
+
async (c) => {
|
|
65358
|
+
const env2 = c.env;
|
|
65359
|
+
const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
|
|
65360
|
+
if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_ACCESS_TOKEN) {
|
|
65361
|
+
return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful credentials missing" } }, 400);
|
|
65362
|
+
}
|
|
65363
|
+
const db = createDb(env2.DB);
|
|
65364
|
+
const contentful = new ContentfulService(CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
65365
|
+
try {
|
|
65366
|
+
if (CONTENTFUL_MANAGEMENT_TOKEN) {
|
|
65367
|
+
const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
65368
|
+
await ensureContentTypes(mgmt);
|
|
65369
|
+
}
|
|
65370
|
+
c.executionCtx.waitUntil(
|
|
65371
|
+
snapshotAllContent(db, env2.FILES, contentful, {}, env2.KV).then((r) => console.log("[Contentful] Snapshot complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Snapshot failed:", err))
|
|
65372
|
+
);
|
|
65373
|
+
return c.json({ message: "Contentful sync triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
|
|
65374
|
+
} catch (err) {
|
|
65375
|
+
console.error("[Contentful] Sync trigger failed:", err);
|
|
65376
|
+
return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger sync" } }, 500);
|
|
65377
|
+
}
|
|
65378
|
+
}
|
|
65379
|
+
);
|
|
65380
|
+
contentfulSyncRoute.post(
|
|
65381
|
+
"/push",
|
|
65382
|
+
authMiddleware,
|
|
65383
|
+
adminMiddleware,
|
|
65384
|
+
async (c) => {
|
|
65385
|
+
const env2 = c.env;
|
|
65386
|
+
const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
|
|
65387
|
+
if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
|
|
65388
|
+
return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful management credentials missing" } }, 400);
|
|
65389
|
+
}
|
|
65390
|
+
const db = createDb(env2.DB);
|
|
65391
|
+
const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
65392
|
+
try {
|
|
65393
|
+
c.executionCtx.waitUntil(
|
|
65394
|
+
pushToContentful(db, mgmt, {}, env2.KV).then((r) => console.log("[Contentful] Push complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Push failed:", err))
|
|
65395
|
+
);
|
|
65396
|
+
return c.json({ message: "Contentful push triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
|
|
65397
|
+
} catch (err) {
|
|
65398
|
+
console.error("[Contentful] Push trigger failed:", err);
|
|
65399
|
+
return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger push" } }, 500);
|
|
65400
|
+
}
|
|
65401
|
+
}
|
|
65402
|
+
);
|
|
65403
|
+
contentfulSyncRoute.get(
|
|
65404
|
+
"/status",
|
|
65405
|
+
authMiddleware,
|
|
65406
|
+
adminMiddleware,
|
|
65407
|
+
async (c) => {
|
|
65408
|
+
const env2 = c.env;
|
|
65409
|
+
const db = createDb(env2.DB);
|
|
65410
|
+
const [[{ themes: themes2 }], [{ events: events2 }], [{ faqs: faqs2 }], [{ orgs }]] = await Promise.all([
|
|
65411
|
+
db.all(sql2`SELECT count(*) as themes FROM themes`),
|
|
65412
|
+
db.all(sql2`SELECT count(*) as events FROM events`),
|
|
65413
|
+
db.all(sql2`SELECT count(*) as faqs FROM faqs`),
|
|
65414
|
+
db.all(sql2`SELECT count(*) as orgs FROM organizations`)
|
|
65415
|
+
]);
|
|
65416
|
+
const isConfigured = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_ACCESS_TOKEN;
|
|
65417
|
+
const canPush = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_MANAGEMENT_TOKEN;
|
|
65418
|
+
return c.json({
|
|
65419
|
+
isConfigured,
|
|
65420
|
+
canPush,
|
|
65421
|
+
cmsMode: c.get("cmsMode"),
|
|
65422
|
+
spaceId: env2.CONTENTFUL_SPACE_ID ?? null,
|
|
65423
|
+
totals: { themes: themes2, events: events2, faqs: faqs2, organizations: orgs }
|
|
65424
|
+
});
|
|
65425
|
+
}
|
|
65426
|
+
);
|
|
65427
|
+
|
|
64862
65428
|
// src/app.ts
|
|
64863
65429
|
function createApp(options = {}) {
|
|
64864
65430
|
const app2 = new Hono2();
|
|
@@ -64872,6 +65438,12 @@ function createApp(options = {}) {
|
|
|
64872
65438
|
app2.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
|
|
64873
65439
|
app2.use("*", createPowChallengeMiddleware());
|
|
64874
65440
|
app2.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
|
|
65441
|
+
app2.use("*", async (c, next) => {
|
|
65442
|
+
const overrideRaw = await c.env.KV.get("config:cms_mode").catch(() => null);
|
|
65443
|
+
const override = overrideRaw ? JSON.parse(overrideRaw) : null;
|
|
65444
|
+
c.set("cmsMode", parseCmsMode(override ?? c.env.CMS_MODE));
|
|
65445
|
+
return next();
|
|
65446
|
+
});
|
|
64875
65447
|
app2.on(["POST", "GET"], "/api/auth/*", (c) => {
|
|
64876
65448
|
const auth = createAuth(c.env);
|
|
64877
65449
|
const req = c.req.raw;
|
|
@@ -64883,7 +65455,7 @@ function createApp(options = {}) {
|
|
|
64883
65455
|
return auth.handler(new Request(req.url, {
|
|
64884
65456
|
method: req.method,
|
|
64885
65457
|
headers: newHeaders,
|
|
64886
|
-
body: req.method === "GET" ? null : req.body,
|
|
65458
|
+
body: req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" ? null : req.body,
|
|
64887
65459
|
redirect: req.redirect
|
|
64888
65460
|
}));
|
|
64889
65461
|
}
|
|
@@ -64904,12 +65476,13 @@ function createApp(options = {}) {
|
|
|
64904
65476
|
app2.post(POW_VERIFY_PATH, handlePowVerify);
|
|
64905
65477
|
app2.route("/health", healthRoute);
|
|
64906
65478
|
app2.route("/api/config", siteConfigRoute);
|
|
64907
|
-
app2.route("/api/
|
|
65479
|
+
app2.route("/api/classes", classesRoute);
|
|
64908
65480
|
app2.route("/api/themes", themesRoute);
|
|
64909
65481
|
app2.route("/api/users", usersRoute);
|
|
64910
65482
|
app2.route("/api/organizations", organizationsRoute);
|
|
64911
65483
|
app2.route("/api/faqs", faqsRoute);
|
|
64912
65484
|
app2.route("/api/uploads", uploadsRoute);
|
|
65485
|
+
app2.route("/api/contentful", contentfulSyncRoute);
|
|
64913
65486
|
app2.route("/internal/gforms-webhook", gformsWebhookRoute);
|
|
64914
65487
|
app2.onError(errorHandler2);
|
|
64915
65488
|
app2.notFound(
|
|
@@ -65543,6 +66116,7 @@ var PATCH_STATEMENTS = [
|
|
|
65543
66116
|
`ALTER TABLE "events" ADD COLUMN "class_code" text`,
|
|
65544
66117
|
`ALTER TABLE "events" ADD COLUMN "start_time" text`,
|
|
65545
66118
|
`ALTER TABLE "events" ADD COLUMN "end_time" text`,
|
|
66119
|
+
`ALTER TABLE "events" RENAME COLUMN "is_major" TO "is_spotlight"`,
|
|
65546
66120
|
`CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
|
|
65547
66121
|
];
|
|
65548
66122
|
var CREATE_STATEMENTS = [
|
|
@@ -65646,7 +66220,7 @@ var CREATE_STATEMENTS = [
|
|
|
65646
66220
|
"class_code" text,
|
|
65647
66221
|
"start_time" text,
|
|
65648
66222
|
"end_time" text,
|
|
65649
|
-
"
|
|
66223
|
+
"is_spotlight" integer DEFAULT false NOT NULL,
|
|
65650
66224
|
"max_slots" integer DEFAULT 0 NOT NULL,
|
|
65651
66225
|
"registered_slots" integer DEFAULT 0 NOT NULL,
|
|
65652
66226
|
"gforms_id" text,
|
|
@@ -65708,7 +66282,9 @@ async function ensureDatabase(d1) {
|
|
|
65708
66282
|
try {
|
|
65709
66283
|
await d1.prepare(sql3).run();
|
|
65710
66284
|
} catch (err) {
|
|
65711
|
-
if (err?.message?.includes("duplicate column"))
|
|
66285
|
+
if (err?.message?.includes("duplicate column") || err?.message?.includes("no such column: is_major")) {
|
|
66286
|
+
continue;
|
|
66287
|
+
}
|
|
65712
66288
|
throw err;
|
|
65713
66289
|
}
|
|
65714
66290
|
}
|