@access-dlsu/leapify 0.260507.1 → 0.260507.5
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/auth/auth.d.ts.map +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/{chunk-ANNHE3PZ.js → chunk-5YYVBPAE.js} +21 -5
- package/dist/chunk-5YYVBPAE.js.map +1 -0
- package/dist/{chunk-QARF2YFF.cjs → chunk-LVKPYSXI.cjs} +21 -5
- package/dist/chunk-LVKPYSXI.cjs.map +1 -0
- package/dist/{chunk-63CUZGSZ.js → chunk-OZ6HZKR5.js} +21 -5
- package/dist/chunk-OZ6HZKR5.js.map +1 -0
- package/dist/{chunk-YFJBE3AU.cjs → chunk-S5DBMZVP.cjs} +21 -5
- package/dist/chunk-S5DBMZVP.cjs.map +1 -0
- package/dist/client/auth.d.ts +1 -13
- package/dist/client/auth.d.ts.map +1 -1
- 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 +679 -59
- 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 +655 -40
- package/dist/index.js.map +1 -1
- package/dist/lib/middleware/cors.d.ts.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 +662 -39
- package/dist/worker.js.map +1 -1
- package/package.json +153 -153
- package/dist/chunk-63CUZGSZ.js.map +0 -1
- package/dist/chunk-ANNHE3PZ.js.map +0 -1
- package/dist/chunk-QARF2YFF.cjs.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,13 +25089,22 @@ 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
|
-
const
|
|
25100
|
+
const defaults = {
|
|
25095
25101
|
origin: "*",
|
|
25096
25102
|
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
25097
25103
|
allowHeaders: [],
|
|
25098
|
-
exposeHeaders: []
|
|
25104
|
+
exposeHeaders: []
|
|
25105
|
+
};
|
|
25106
|
+
const opts = {
|
|
25107
|
+
...defaults,
|
|
25099
25108
|
...options
|
|
25100
25109
|
};
|
|
25101
25110
|
const findAllowOrigin = ((optsOrigin) => {
|
|
@@ -25186,9 +25195,22 @@ function createCorsMiddleware(allowedOrigins) {
|
|
|
25186
25195
|
});
|
|
25187
25196
|
return async (c, next) => {
|
|
25188
25197
|
const origin = c.req.header("origin");
|
|
25198
|
+
if (c.req.path.startsWith("/api/uploads/images")) {
|
|
25199
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
25200
|
+
c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
25201
|
+
if (c.req.method === "OPTIONS") {
|
|
25202
|
+
return c.body(null, 204);
|
|
25203
|
+
}
|
|
25204
|
+
return next();
|
|
25205
|
+
}
|
|
25189
25206
|
if (!c.req.path.startsWith("/health") && !c.req.path.startsWith("/api/auth") && !c.req.path.startsWith("/internal") && origin && !allowedOrigins.includes("*") && !allowedOrigins.includes(origin)) {
|
|
25190
25207
|
return c.json(
|
|
25191
|
-
{
|
|
25208
|
+
{
|
|
25209
|
+
error: {
|
|
25210
|
+
code: "DOMAIN_RESTRICTED",
|
|
25211
|
+
message: `Origin ${origin} is not allowed`
|
|
25212
|
+
}
|
|
25213
|
+
},
|
|
25192
25214
|
403
|
|
25193
25215
|
);
|
|
25194
25216
|
}
|
|
@@ -25224,7 +25246,15 @@ var CHALLENGE_KV_PREFIX = "pow:challenge:";
|
|
|
25224
25246
|
var DEFAULT_POW_DIFFICULTY = 4;
|
|
25225
25247
|
var CHALLENGE_TTL_SEC = 120;
|
|
25226
25248
|
var COOKIE_MAX_AGE_SEC = 3600;
|
|
25227
|
-
var EXEMPT_PATHS = [
|
|
25249
|
+
var EXEMPT_PATHS = [
|
|
25250
|
+
"/health",
|
|
25251
|
+
"/internal",
|
|
25252
|
+
"/api/auth",
|
|
25253
|
+
"/api/uploads/images",
|
|
25254
|
+
"/api/classes",
|
|
25255
|
+
"/api/faqs",
|
|
25256
|
+
"/api/config"
|
|
25257
|
+
];
|
|
25228
25258
|
function base64urlEncode(bytes) {
|
|
25229
25259
|
let binary2 = "";
|
|
25230
25260
|
for (const byte of bytes) {
|
|
@@ -25389,16 +25419,24 @@ async function handlePowVerify(c) {
|
|
|
25389
25419
|
const secret = c.env.INTERNAL_API_SECRET;
|
|
25390
25420
|
const ip = getClientIp(c);
|
|
25391
25421
|
const token = await signCookie(secret, ip);
|
|
25422
|
+
const isSecure = c.req.raw.url.startsWith("https") || c.req.header("x-forwarded-proto") === "https";
|
|
25392
25423
|
c.header(
|
|
25393
25424
|
"Set-Cookie",
|
|
25394
|
-
`${POW_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; Secure; HttpOnly; SameSite=Lax`
|
|
25425
|
+
`${POW_COOKIE_NAME}=${token}; Path=/; Max-Age=${COOKIE_MAX_AGE_SEC}; ${isSecure ? "Secure; " : ""}HttpOnly; SameSite=Lax`
|
|
25395
25426
|
);
|
|
25396
25427
|
return c.json({ redirect: redir || "/" });
|
|
25397
25428
|
}
|
|
25398
25429
|
function createPowChallengeMiddleware() {
|
|
25399
25430
|
return createMiddleware(async (c, next) => {
|
|
25400
25431
|
if (c.req.path === POW_VERIFY_PATH) return next();
|
|
25401
|
-
|
|
25432
|
+
const normalizedPath = c.req.path.toLowerCase().replace(/\/$/, "");
|
|
25433
|
+
const isExempt = EXEMPT_PATHS.some((p) => {
|
|
25434
|
+
const ep = p.toLowerCase().replace(/\/$/, "");
|
|
25435
|
+
return normalizedPath === ep || normalizedPath.startsWith(ep + "/");
|
|
25436
|
+
});
|
|
25437
|
+
console.log(`[pow] path=${c.req.path} normalized=${normalizedPath} exempt=${isExempt}`);
|
|
25438
|
+
if (isExempt) return next();
|
|
25439
|
+
if (c.req.method === "OPTIONS") return next();
|
|
25402
25440
|
if (c.req.header("Authorization")) return next();
|
|
25403
25441
|
const cookieHeader = c.req.header("Cookie") ?? "";
|
|
25404
25442
|
const cookieMatch = cookieHeader.match(
|
|
@@ -62511,12 +62549,14 @@ function drizzle(client, config3 = {}) {
|
|
|
62511
62549
|
// src/db/schema/index.ts
|
|
62512
62550
|
var schema_exports = {};
|
|
62513
62551
|
__export(schema_exports, {
|
|
62552
|
+
CONTENTFUL_CONFIG_KEYS: () => CONTENTFUL_CONFIG_KEYS,
|
|
62514
62553
|
authAccount: () => authAccount,
|
|
62515
62554
|
authSession: () => authSession,
|
|
62516
62555
|
authUser: () => authUser,
|
|
62517
62556
|
authVerification: () => authVerification,
|
|
62518
62557
|
bookmarks: () => bookmarks,
|
|
62519
62558
|
bookmarksRelations: () => bookmarksRelations,
|
|
62559
|
+
contentfulConfig: () => contentfulConfig,
|
|
62520
62560
|
events: () => events,
|
|
62521
62561
|
eventsRelations: () => eventsRelations,
|
|
62522
62562
|
faqs: () => faqs,
|
|
@@ -62568,7 +62608,7 @@ var organizationsRelations = relations(organizations, ({ many }) => ({
|
|
|
62568
62608
|
events: many(events)
|
|
62569
62609
|
}));
|
|
62570
62610
|
|
|
62571
|
-
// src/db/schema/
|
|
62611
|
+
// src/db/schema/classes.ts
|
|
62572
62612
|
var events = sqliteTable(
|
|
62573
62613
|
"events",
|
|
62574
62614
|
{
|
|
@@ -62593,7 +62633,7 @@ var events = sqliteTable(
|
|
|
62593
62633
|
// start time string
|
|
62594
62634
|
endTime: text("end_time"),
|
|
62595
62635
|
// end time string
|
|
62596
|
-
|
|
62636
|
+
isSpotlight: integer2("is_spotlight", { mode: "boolean" }).notNull().default(false),
|
|
62597
62637
|
// Slot tracking (local counter — NOT polled from Google Forms)
|
|
62598
62638
|
maxSlots: integer2("max_slots").notNull().default(0),
|
|
62599
62639
|
registeredSlots: integer2("registered_slots").notNull().default(0),
|
|
@@ -62650,6 +62690,18 @@ var siteConfig = sqliteTable("site_config", {
|
|
|
62650
62690
|
// JSON-serializable string
|
|
62651
62691
|
updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
|
|
62652
62692
|
});
|
|
62693
|
+
var CONTENTFUL_CONFIG_KEYS = {
|
|
62694
|
+
ENABLED: "contentful.enabled",
|
|
62695
|
+
SPACE_ID: "contentful.spaceId",
|
|
62696
|
+
MANAGEMENT_TOKEN: "contentful.managementToken",
|
|
62697
|
+
DEFAULT_SPACE_ID: "dlsu-events"
|
|
62698
|
+
};
|
|
62699
|
+
var contentfulConfig = sqliteTable("contentful_config", {
|
|
62700
|
+
space_id: text("space_id"),
|
|
62701
|
+
contentful_enabled: integer2("contentful_enabled").notNull().default(0),
|
|
62702
|
+
last_sync_at: integer2("last_sync_at"),
|
|
62703
|
+
updated_at: integer2("updated_at").default(sql2`(unixepoch())`).notNull()
|
|
62704
|
+
});
|
|
62653
62705
|
|
|
62654
62706
|
// src/db/schema/faqs.ts
|
|
62655
62707
|
var faqs = sqliteTable("faqs", {
|
|
@@ -62819,11 +62871,21 @@ function createAuth(env2) {
|
|
|
62819
62871
|
};
|
|
62820
62872
|
if (isFirstUser) {
|
|
62821
62873
|
await db.insert(users).values(base).onConflictDoUpdate({
|
|
62822
|
-
target: users.
|
|
62823
|
-
set: {
|
|
62874
|
+
target: users.email,
|
|
62875
|
+
set: {
|
|
62876
|
+
betterAuthId: user.id,
|
|
62877
|
+
role: "super_admin",
|
|
62878
|
+
name: user.name ?? user.email.split("@")[0]
|
|
62879
|
+
}
|
|
62824
62880
|
});
|
|
62825
62881
|
} else {
|
|
62826
|
-
await db.insert(users).values(base).
|
|
62882
|
+
await db.insert(users).values(base).onConflictDoUpdate({
|
|
62883
|
+
target: users.email,
|
|
62884
|
+
set: {
|
|
62885
|
+
betterAuthId: user.id,
|
|
62886
|
+
name: user.name ?? user.email.split("@")[0]
|
|
62887
|
+
}
|
|
62888
|
+
});
|
|
62827
62889
|
}
|
|
62828
62890
|
}
|
|
62829
62891
|
}
|
|
@@ -62852,7 +62914,13 @@ async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAu
|
|
|
62852
62914
|
betterAuthId: betterAuthUserId,
|
|
62853
62915
|
email: betterAuthUserEmail,
|
|
62854
62916
|
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
|
|
62855
|
-
}).
|
|
62917
|
+
}).onConflictDoUpdate({
|
|
62918
|
+
target: users.email,
|
|
62919
|
+
set: {
|
|
62920
|
+
betterAuthId: betterAuthUserId,
|
|
62921
|
+
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
|
|
62922
|
+
}
|
|
62923
|
+
}).returning();
|
|
62856
62924
|
dbUser = created;
|
|
62857
62925
|
}
|
|
62858
62926
|
if (!dbUser) throw unauthorized("Failed to resolve user record");
|
|
@@ -63819,7 +63887,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
|
|
|
63819
63887
|
identifier: "uid"
|
|
63820
63888
|
});
|
|
63821
63889
|
|
|
63822
|
-
// src/routes/
|
|
63890
|
+
// src/routes/classes.ts
|
|
63823
63891
|
var EVENTS_LIST_KV_KEY = "events:list";
|
|
63824
63892
|
var EVENTS_ETAG_KV_KEY = "events:etag";
|
|
63825
63893
|
var EVENTS_LIST_TTL = 300;
|
|
@@ -63835,7 +63903,7 @@ async function pushEventToContentful(env2, event) {
|
|
|
63835
63903
|
const fields = {
|
|
63836
63904
|
title: ContentfulManagement.locale(event.title),
|
|
63837
63905
|
slug: ContentfulManagement.locale(event.slug),
|
|
63838
|
-
|
|
63906
|
+
isSpotlight: ContentfulManagement.locale(event.isSpotlight),
|
|
63839
63907
|
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
63840
63908
|
};
|
|
63841
63909
|
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
@@ -63898,7 +63966,7 @@ var createEventSchema = external_exports.object({
|
|
|
63898
63966
|
startTime: external_exports.string().optional(),
|
|
63899
63967
|
endTime: external_exports.string().optional(),
|
|
63900
63968
|
registrationClosesAt: external_exports.number().optional(),
|
|
63901
|
-
|
|
63969
|
+
isSpotlight: external_exports.boolean().default(false),
|
|
63902
63970
|
maxSlots: external_exports.number().int().min(0).default(0),
|
|
63903
63971
|
gformsId: external_exports.string().optional(),
|
|
63904
63972
|
gformsUrl: external_exports.string().url().optional(),
|
|
@@ -63907,11 +63975,11 @@ var createEventSchema = external_exports.object({
|
|
|
63907
63975
|
contentfulEntryId: external_exports.string().optional(),
|
|
63908
63976
|
status: external_exports.enum(["draft", "queued", "published"]).default("draft")
|
|
63909
63977
|
});
|
|
63910
|
-
var
|
|
63978
|
+
var classesRoute = new Hono2();
|
|
63911
63979
|
function generateSlug(title) {
|
|
63912
63980
|
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
63913
63981
|
}
|
|
63914
|
-
|
|
63982
|
+
classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
|
|
63915
63983
|
const db = createDb(c.env.DB);
|
|
63916
63984
|
const data = await db.query.events.findMany({
|
|
63917
63985
|
with: { theme: true, organization: true },
|
|
@@ -63919,7 +63987,7 @@ eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
|
|
|
63919
63987
|
});
|
|
63920
63988
|
return c.json({ data });
|
|
63921
63989
|
});
|
|
63922
|
-
|
|
63990
|
+
classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
|
|
63923
63991
|
const body = await c.req.json();
|
|
63924
63992
|
const db = createDb(c.env.DB);
|
|
63925
63993
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -63947,7 +64015,7 @@ eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) =>
|
|
|
63947
64015
|
]);
|
|
63948
64016
|
return c.json({ data: { updated: body.ids.length } });
|
|
63949
64017
|
});
|
|
63950
|
-
|
|
64018
|
+
classesRoute.get("/", eventsListRateLimit, async (c) => {
|
|
63951
64019
|
const db = createDb(c.env.DB);
|
|
63952
64020
|
const cache3 = new CacheService(c.env.KV);
|
|
63953
64021
|
const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
|
|
@@ -63958,7 +64026,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
63958
64026
|
);
|
|
63959
64027
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
63960
64028
|
if (ifNoneMatch === etag) {
|
|
63961
|
-
return c.
|
|
64029
|
+
return c.body(null, 304);
|
|
63962
64030
|
}
|
|
63963
64031
|
const data = await cache3.getOrSet(
|
|
63964
64032
|
EVENTS_LIST_KV_KEY,
|
|
@@ -63982,7 +64050,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
63982
64050
|
startTime: true,
|
|
63983
64051
|
endTime: true,
|
|
63984
64052
|
registrationClosesAt: true,
|
|
63985
|
-
|
|
64053
|
+
isSpotlight: true,
|
|
63986
64054
|
maxSlots: true,
|
|
63987
64055
|
registeredSlots: true,
|
|
63988
64056
|
gformsUrl: true,
|
|
@@ -63999,7 +64067,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
63999
64067
|
);
|
|
64000
64068
|
return c.json({ data });
|
|
64001
64069
|
});
|
|
64002
|
-
|
|
64070
|
+
classesRoute.get("/:slug", async (c) => {
|
|
64003
64071
|
const { slug } = c.req.param();
|
|
64004
64072
|
const db = createDb(c.env.DB);
|
|
64005
64073
|
const event = await db.query.events.findFirst({
|
|
@@ -64011,7 +64079,7 @@ eventsRoute.get("/:slug", async (c) => {
|
|
|
64011
64079
|
if (!event) throw notFound("Event");
|
|
64012
64080
|
return c.json({ data: event });
|
|
64013
64081
|
});
|
|
64014
|
-
|
|
64082
|
+
classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
64015
64083
|
const { slug } = c.req.param();
|
|
64016
64084
|
const db = createDb(c.env.DB);
|
|
64017
64085
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -64021,7 +64089,7 @@ eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
|
64021
64089
|
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
64022
64090
|
return c.json({ data: info2 });
|
|
64023
64091
|
});
|
|
64024
|
-
|
|
64092
|
+
classesRoute.post(
|
|
64025
64093
|
"/",
|
|
64026
64094
|
authMiddleware,
|
|
64027
64095
|
adminMiddleware,
|
|
@@ -64056,11 +64124,13 @@ eventsRoute.post(
|
|
|
64056
64124
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
64057
64125
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
64058
64126
|
]);
|
|
64059
|
-
c.
|
|
64127
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
64128
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
|
|
64129
|
+
}
|
|
64060
64130
|
return c.json({ data: created }, 201);
|
|
64061
64131
|
}
|
|
64062
64132
|
);
|
|
64063
|
-
|
|
64133
|
+
classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
64064
64134
|
const { slug } = c.req.param();
|
|
64065
64135
|
const body = await c.req.json();
|
|
64066
64136
|
const db = createDb(c.env.DB);
|
|
@@ -64075,10 +64145,12 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64075
64145
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
64076
64146
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
64077
64147
|
]);
|
|
64078
|
-
c.
|
|
64148
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
64149
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
|
|
64150
|
+
}
|
|
64079
64151
|
return c.json({ data: updated });
|
|
64080
64152
|
});
|
|
64081
|
-
|
|
64153
|
+
classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
64082
64154
|
const { slug } = c.req.param();
|
|
64083
64155
|
const db = createDb(c.env.DB);
|
|
64084
64156
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -64177,7 +64249,447 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
|
|
|
64177
64249
|
return c.json({ data: { bookmarked: false } });
|
|
64178
64250
|
});
|
|
64179
64251
|
|
|
64252
|
+
// src/services/contentful.ts
|
|
64253
|
+
var CONTENTFUL_CDN = "https://cdn.contentful.com";
|
|
64254
|
+
var ContentfulService = class _ContentfulService {
|
|
64255
|
+
spaceId;
|
|
64256
|
+
accessToken;
|
|
64257
|
+
environment;
|
|
64258
|
+
constructor(spaceId, accessToken, environment = "master") {
|
|
64259
|
+
this.spaceId = spaceId;
|
|
64260
|
+
this.accessToken = accessToken;
|
|
64261
|
+
this.environment = environment;
|
|
64262
|
+
}
|
|
64263
|
+
/**
|
|
64264
|
+
* Returns true if the required Contentful credentials are configured.
|
|
64265
|
+
*/
|
|
64266
|
+
static isConfigured(spaceId, accessToken) {
|
|
64267
|
+
return !!(spaceId && accessToken);
|
|
64268
|
+
}
|
|
64269
|
+
// ─── Entries ─────────────────────────────────────────────────────────────
|
|
64270
|
+
/**
|
|
64271
|
+
* Fetch all entries of a given content type.
|
|
64272
|
+
* Handles pagination automatically (100 per page, Contentful max).
|
|
64273
|
+
*/
|
|
64274
|
+
async getEntries(contentTypeId) {
|
|
64275
|
+
const allItems = [];
|
|
64276
|
+
let skip = 0;
|
|
64277
|
+
const limit = 100;
|
|
64278
|
+
do {
|
|
64279
|
+
const url2 = this.buildUrl(`/entries`, {
|
|
64280
|
+
content_type: contentTypeId,
|
|
64281
|
+
skip: String(skip),
|
|
64282
|
+
limit: String(limit),
|
|
64283
|
+
include: "2"
|
|
64284
|
+
// resolve up to 2 levels of linked entries/assets
|
|
64285
|
+
});
|
|
64286
|
+
const res = await fetch(url2, { headers: this.headers() });
|
|
64287
|
+
if (!res.ok) {
|
|
64288
|
+
throw new Error(`Contentful entries error: ${res.status} ${await res.text()}`);
|
|
64289
|
+
}
|
|
64290
|
+
const data = await res.json();
|
|
64291
|
+
allItems.push(...data.items);
|
|
64292
|
+
skip += limit;
|
|
64293
|
+
if (allItems.length >= data.total) break;
|
|
64294
|
+
} while (true);
|
|
64295
|
+
return allItems;
|
|
64296
|
+
}
|
|
64297
|
+
/**
|
|
64298
|
+
* Fetch all assets. Handles pagination.
|
|
64299
|
+
*/
|
|
64300
|
+
async getAssets() {
|
|
64301
|
+
const allItems = [];
|
|
64302
|
+
let skip = 0;
|
|
64303
|
+
const limit = 100;
|
|
64304
|
+
do {
|
|
64305
|
+
const url2 = this.buildUrl(`/assets`, {
|
|
64306
|
+
skip: String(skip),
|
|
64307
|
+
limit: String(limit)
|
|
64308
|
+
});
|
|
64309
|
+
const res = await fetch(url2, { headers: this.headers() });
|
|
64310
|
+
if (!res.ok) {
|
|
64311
|
+
throw new Error(`Contentful assets error: ${res.status} ${await res.text()}`);
|
|
64312
|
+
}
|
|
64313
|
+
const data = await res.json();
|
|
64314
|
+
allItems.push(...data.items);
|
|
64315
|
+
skip += limit;
|
|
64316
|
+
if (allItems.length >= data.total) break;
|
|
64317
|
+
} while (true);
|
|
64318
|
+
return allItems;
|
|
64319
|
+
}
|
|
64320
|
+
// ─── Asset file download ─────────────────────────────────────────────────
|
|
64321
|
+
/**
|
|
64322
|
+
* Download an asset file from Contentful's CDN.
|
|
64323
|
+
* Returns the raw ArrayBuffer and content type.
|
|
64324
|
+
*/
|
|
64325
|
+
async downloadAsset(assetUrl) {
|
|
64326
|
+
const url2 = assetUrl.startsWith("//") ? `https:${assetUrl}` : assetUrl;
|
|
64327
|
+
const res = await fetch(url2);
|
|
64328
|
+
if (!res.ok) {
|
|
64329
|
+
throw new Error(`Failed to download asset: ${res.status}`);
|
|
64330
|
+
}
|
|
64331
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
64332
|
+
const data = await res.arrayBuffer();
|
|
64333
|
+
return { data, contentType };
|
|
64334
|
+
}
|
|
64335
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
64336
|
+
/**
|
|
64337
|
+
* Extract a field value from a Contentful entry, handling locale wrapping.
|
|
64338
|
+
* Contentful fields are often `{ "en-US": value }` — this unwraps them.
|
|
64339
|
+
*/
|
|
64340
|
+
static getField(entry, fieldName) {
|
|
64341
|
+
const raw2 = entry.fields[fieldName];
|
|
64342
|
+
if (raw2 === void 0 || raw2 === null) return void 0;
|
|
64343
|
+
if (typeof raw2 === "object" && !Array.isArray(raw2) && "en-US" in raw2) {
|
|
64344
|
+
return raw2["en-US"];
|
|
64345
|
+
}
|
|
64346
|
+
return raw2;
|
|
64347
|
+
}
|
|
64348
|
+
/**
|
|
64349
|
+
* Extract a linked entry/sys reference ID from a reference field.
|
|
64350
|
+
*/
|
|
64351
|
+
static getRefId(entry, fieldName) {
|
|
64352
|
+
const ref = _ContentfulService.getField(entry, fieldName);
|
|
64353
|
+
return ref?.sys?.id;
|
|
64354
|
+
}
|
|
64355
|
+
/**
|
|
64356
|
+
* Extract an asset URL from a linked asset field.
|
|
64357
|
+
*/
|
|
64358
|
+
static getAssetUrl(entry, fieldName) {
|
|
64359
|
+
const asset = _ContentfulService.getField(entry, fieldName);
|
|
64360
|
+
return asset?.sys?.id;
|
|
64361
|
+
}
|
|
64362
|
+
/**
|
|
64363
|
+
* Resolve an asset URL by ID from a list of fetched assets.
|
|
64364
|
+
*/
|
|
64365
|
+
static resolveAssetUrl(assets, assetId) {
|
|
64366
|
+
const asset = assets.find((a) => a.sys.id === assetId);
|
|
64367
|
+
return asset?.fields?.file?.url;
|
|
64368
|
+
}
|
|
64369
|
+
// ─── Private ─────────────────────────────────────────────────────────────
|
|
64370
|
+
headers() {
|
|
64371
|
+
return {
|
|
64372
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
64373
|
+
"Content-Type": "application/json"
|
|
64374
|
+
};
|
|
64375
|
+
}
|
|
64376
|
+
buildUrl(path, params = {}) {
|
|
64377
|
+
const url2 = new URL(
|
|
64378
|
+
`/spaces/${this.spaceId}/environments/${this.environment}${path}`,
|
|
64379
|
+
CONTENTFUL_CDN
|
|
64380
|
+
);
|
|
64381
|
+
for (const [key, value] of Object.entries(params)) {
|
|
64382
|
+
url2.searchParams.set(key, value);
|
|
64383
|
+
}
|
|
64384
|
+
return url2.toString();
|
|
64385
|
+
}
|
|
64386
|
+
};
|
|
64387
|
+
|
|
64180
64388
|
// src/services/snapshot.ts
|
|
64389
|
+
var DEFAULT_FIELDS = {
|
|
64390
|
+
event: {
|
|
64391
|
+
title: "title",
|
|
64392
|
+
slug: "slug",
|
|
64393
|
+
theme: "theme",
|
|
64394
|
+
organization: "organization",
|
|
64395
|
+
venue: "venue",
|
|
64396
|
+
date: "date",
|
|
64397
|
+
startTime: "startTime",
|
|
64398
|
+
endTime: "endTime",
|
|
64399
|
+
price: "price",
|
|
64400
|
+
image: "image",
|
|
64401
|
+
isSpotlight: "isSpotlight",
|
|
64402
|
+
maxSlots: "maxSlots",
|
|
64403
|
+
gformsUrl: "gformsUrl",
|
|
64404
|
+
gformsEditorUrl: "gformsEditorUrl",
|
|
64405
|
+
registrationClosesAt: "registrationClosesAt",
|
|
64406
|
+
classCode: "classCode"
|
|
64407
|
+
},
|
|
64408
|
+
theme: {
|
|
64409
|
+
name: "name",
|
|
64410
|
+
path: "path"
|
|
64411
|
+
},
|
|
64412
|
+
faq: {
|
|
64413
|
+
question: "question",
|
|
64414
|
+
answer: "answer",
|
|
64415
|
+
category: "category",
|
|
64416
|
+
sortOrder: "sortOrder"
|
|
64417
|
+
},
|
|
64418
|
+
organization: {
|
|
64419
|
+
name: "name",
|
|
64420
|
+
acronym: "acronym",
|
|
64421
|
+
logoUrl: "logoUrl",
|
|
64422
|
+
link: "link"
|
|
64423
|
+
},
|
|
64424
|
+
siteConfig: {
|
|
64425
|
+
key: "key",
|
|
64426
|
+
value: "value"
|
|
64427
|
+
}
|
|
64428
|
+
};
|
|
64429
|
+
var CONTENTFUL_CACHE_PREFIX = "contentful:cache";
|
|
64430
|
+
var CONTENTFUL_CACHE_TTL = 300;
|
|
64431
|
+
async function snapshotAllContent(db, bucket, contentful, config3 = {}, kv) {
|
|
64432
|
+
const mergedConfig = {
|
|
64433
|
+
eventTypeId: config3.eventTypeId ?? "event",
|
|
64434
|
+
themeTypeId: config3.themeTypeId ?? "theme",
|
|
64435
|
+
faqTypeId: config3.faqTypeId ?? "faq",
|
|
64436
|
+
organizationTypeId: config3.organizationTypeId ?? "organization",
|
|
64437
|
+
fields: {
|
|
64438
|
+
event: { ...DEFAULT_FIELDS.event, ...config3.fields?.event },
|
|
64439
|
+
theme: { ...DEFAULT_FIELDS.theme, ...config3.fields?.theme },
|
|
64440
|
+
faq: { ...DEFAULT_FIELDS.faq, ...config3.fields?.faq },
|
|
64441
|
+
organization: { ...DEFAULT_FIELDS.organization, ...config3.fields?.organization },
|
|
64442
|
+
siteConfig: { ...DEFAULT_FIELDS.siteConfig, ...config3.fields?.siteConfig }
|
|
64443
|
+
}
|
|
64444
|
+
};
|
|
64445
|
+
const result = {
|
|
64446
|
+
themesSynced: 0,
|
|
64447
|
+
eventsSynced: 0,
|
|
64448
|
+
faqsSynced: 0,
|
|
64449
|
+
organizationsSynced: 0,
|
|
64450
|
+
imagesUploaded: 0,
|
|
64451
|
+
imagesSkipped: 0,
|
|
64452
|
+
errors: []
|
|
64453
|
+
};
|
|
64454
|
+
let allAssets = [];
|
|
64455
|
+
if (bucket) {
|
|
64456
|
+
try {
|
|
64457
|
+
allAssets = await contentful.getAssets();
|
|
64458
|
+
} catch (err) {
|
|
64459
|
+
result.errors.push(`Failed to fetch assets: ${err}`);
|
|
64460
|
+
}
|
|
64461
|
+
}
|
|
64462
|
+
try {
|
|
64463
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:themes`;
|
|
64464
|
+
let themeEntries;
|
|
64465
|
+
if (kv) {
|
|
64466
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64467
|
+
if (cached2) {
|
|
64468
|
+
themeEntries = cached2;
|
|
64469
|
+
} else {
|
|
64470
|
+
themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
|
|
64471
|
+
await kv.put(cacheKey, JSON.stringify(themeEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64472
|
+
}
|
|
64473
|
+
} else {
|
|
64474
|
+
themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
|
|
64475
|
+
}
|
|
64476
|
+
result.themesSynced = await syncThemes(db, themeEntries, mergedConfig);
|
|
64477
|
+
} catch (err) {
|
|
64478
|
+
result.errors.push(`Themes sync failed: ${err}`);
|
|
64479
|
+
}
|
|
64480
|
+
try {
|
|
64481
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:organizations`;
|
|
64482
|
+
let orgEntries;
|
|
64483
|
+
if (kv) {
|
|
64484
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64485
|
+
if (cached2) {
|
|
64486
|
+
orgEntries = cached2;
|
|
64487
|
+
} else {
|
|
64488
|
+
orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
|
|
64489
|
+
await kv.put(cacheKey, JSON.stringify(orgEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64490
|
+
}
|
|
64491
|
+
} else {
|
|
64492
|
+
orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
|
|
64493
|
+
}
|
|
64494
|
+
await syncOrganizations(db, orgEntries, mergedConfig);
|
|
64495
|
+
} catch (err) {
|
|
64496
|
+
result.errors.push(`Organizations sync failed: ${err}`);
|
|
64497
|
+
}
|
|
64498
|
+
try {
|
|
64499
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:events`;
|
|
64500
|
+
let eventEntries;
|
|
64501
|
+
if (kv) {
|
|
64502
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64503
|
+
if (cached2) {
|
|
64504
|
+
eventEntries = cached2;
|
|
64505
|
+
} else {
|
|
64506
|
+
eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
|
|
64507
|
+
await kv.put(cacheKey, JSON.stringify(eventEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64508
|
+
}
|
|
64509
|
+
} else {
|
|
64510
|
+
eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
|
|
64511
|
+
}
|
|
64512
|
+
result.eventsSynced = await syncEvents(db, bucket, contentful, allAssets, eventEntries, mergedConfig, result);
|
|
64513
|
+
} catch (err) {
|
|
64514
|
+
result.errors.push(`Events sync failed: ${err}`);
|
|
64515
|
+
}
|
|
64516
|
+
try {
|
|
64517
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:faqs`;
|
|
64518
|
+
let faqEntries;
|
|
64519
|
+
if (kv) {
|
|
64520
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
64521
|
+
if (cached2) {
|
|
64522
|
+
faqEntries = cached2;
|
|
64523
|
+
} else {
|
|
64524
|
+
faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
|
|
64525
|
+
await kv.put(cacheKey, JSON.stringify(faqEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
64526
|
+
}
|
|
64527
|
+
} else {
|
|
64528
|
+
faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
|
|
64529
|
+
}
|
|
64530
|
+
result.faqsSynced = await syncFaqs(db, faqEntries, mergedConfig);
|
|
64531
|
+
} catch (err) {
|
|
64532
|
+
result.errors.push(`FAQs sync failed: ${err}`);
|
|
64533
|
+
}
|
|
64534
|
+
console.log(
|
|
64535
|
+
`[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`
|
|
64536
|
+
);
|
|
64537
|
+
return result;
|
|
64538
|
+
}
|
|
64539
|
+
async function syncThemes(db, entries, config3) {
|
|
64540
|
+
let count2 = 0;
|
|
64541
|
+
for (const entry of entries) {
|
|
64542
|
+
const cfId = entry.sys.id;
|
|
64543
|
+
const name = ContentfulService.getField(entry, config3.fields.theme.name);
|
|
64544
|
+
const path = ContentfulService.getField(entry, config3.fields.theme.path);
|
|
64545
|
+
if (!name || !path) continue;
|
|
64546
|
+
await db.insert(themes).values({ id: cfId, name, path }).onConflictDoUpdate({
|
|
64547
|
+
target: themes.id,
|
|
64548
|
+
set: { name, path }
|
|
64549
|
+
});
|
|
64550
|
+
count2++;
|
|
64551
|
+
}
|
|
64552
|
+
return count2;
|
|
64553
|
+
}
|
|
64554
|
+
async function syncOrganizations(db, entries, config3) {
|
|
64555
|
+
let count2 = 0;
|
|
64556
|
+
for (const entry of entries) {
|
|
64557
|
+
const cfId = entry.sys.id;
|
|
64558
|
+
const f = config3.fields.organization;
|
|
64559
|
+
const name = ContentfulService.getField(entry, f.name);
|
|
64560
|
+
const acronym = ContentfulService.getField(entry, f.acronym);
|
|
64561
|
+
if (!name || !acronym) continue;
|
|
64562
|
+
const logoUrl = ContentfulService.getField(entry, f.logoUrl) ?? null;
|
|
64563
|
+
const link = ContentfulService.getField(entry, f.link) ?? null;
|
|
64564
|
+
await db.insert(organizations).values({ id: cfId, name, acronym, logoUrl, link }).onConflictDoUpdate({
|
|
64565
|
+
target: organizations.id,
|
|
64566
|
+
set: { name, acronym, logoUrl, link }
|
|
64567
|
+
});
|
|
64568
|
+
count2++;
|
|
64569
|
+
}
|
|
64570
|
+
return count2;
|
|
64571
|
+
}
|
|
64572
|
+
async function syncEvents(db, bucket, contentful, allAssets, entries, config3, result) {
|
|
64573
|
+
let count2 = 0;
|
|
64574
|
+
for (const entry of entries) {
|
|
64575
|
+
try {
|
|
64576
|
+
const cfId = entry.sys.id;
|
|
64577
|
+
const f = config3.fields.event;
|
|
64578
|
+
const title = ContentfulService.getField(entry, f.title);
|
|
64579
|
+
if (!title) {
|
|
64580
|
+
result.errors.push(`Event ${cfId}: missing title, skipping`);
|
|
64581
|
+
continue;
|
|
64582
|
+
}
|
|
64583
|
+
const slug = ContentfulService.getField(entry, f.slug) ?? slugify2(title);
|
|
64584
|
+
const themeRef = ContentfulService.getField(entry, f.theme);
|
|
64585
|
+
const themeId = themeRef?.sys?.id ?? null;
|
|
64586
|
+
const orgRef = ContentfulService.getField(entry, f.organization);
|
|
64587
|
+
const organizationId = orgRef?.sys?.id ?? null;
|
|
64588
|
+
let backgroundImageUrl = null;
|
|
64589
|
+
if (bucket) {
|
|
64590
|
+
const imageRef = ContentfulService.getField(entry, f.image);
|
|
64591
|
+
const assetId = imageRef?.sys?.id;
|
|
64592
|
+
if (assetId) {
|
|
64593
|
+
const assetUrl = ContentfulService.resolveAssetUrl(allAssets, assetId);
|
|
64594
|
+
if (assetUrl) {
|
|
64595
|
+
const r2Key = `contentful/${assetId}`;
|
|
64596
|
+
const uploaded = await uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key);
|
|
64597
|
+
if (uploaded.skipped) {
|
|
64598
|
+
result.imagesSkipped++;
|
|
64599
|
+
} else {
|
|
64600
|
+
result.imagesUploaded++;
|
|
64601
|
+
}
|
|
64602
|
+
backgroundImageUrl = `/uploads/images/${r2Key}`;
|
|
64603
|
+
}
|
|
64604
|
+
}
|
|
64605
|
+
}
|
|
64606
|
+
const values = {
|
|
64607
|
+
id: cfId,
|
|
64608
|
+
contentfulEntryId: cfId,
|
|
64609
|
+
title,
|
|
64610
|
+
slug,
|
|
64611
|
+
themeId,
|
|
64612
|
+
organizationId,
|
|
64613
|
+
updatedAt: entry.sys.updatedAt
|
|
64614
|
+
};
|
|
64615
|
+
const venue = ContentfulService.getField(entry, f.venue);
|
|
64616
|
+
if (venue !== void 0) values.venue = venue;
|
|
64617
|
+
const date5 = ContentfulService.getField(entry, f.date);
|
|
64618
|
+
if (date5 !== void 0) values.dateTime = date5;
|
|
64619
|
+
const price = ContentfulService.getField(entry, f.price);
|
|
64620
|
+
if (price !== void 0) values.price = price;
|
|
64621
|
+
if (backgroundImageUrl) values.backgroundImageUrl = backgroundImageUrl;
|
|
64622
|
+
const isSpotlight = ContentfulService.getField(entry, f.isSpotlight);
|
|
64623
|
+
if (isSpotlight !== void 0) values.isSpotlight = isSpotlight;
|
|
64624
|
+
const maxSlots = ContentfulService.getField(entry, f.maxSlots);
|
|
64625
|
+
if (maxSlots !== void 0) values.maxSlots = maxSlots;
|
|
64626
|
+
const gformsUrl = ContentfulService.getField(entry, f.gformsUrl);
|
|
64627
|
+
if (gformsUrl !== void 0) values.gformsUrl = gformsUrl;
|
|
64628
|
+
const gformsEditorUrl = ContentfulService.getField(entry, f.gformsEditorUrl);
|
|
64629
|
+
if (gformsEditorUrl !== void 0) values.gformsEditorUrl = gformsEditorUrl;
|
|
64630
|
+
const classCode = ContentfulService.getField(entry, f.classCode);
|
|
64631
|
+
if (classCode !== void 0) values.classCode = classCode;
|
|
64632
|
+
const startTime = ContentfulService.getField(entry, f.startTime);
|
|
64633
|
+
if (startTime !== void 0) values.startTime = startTime;
|
|
64634
|
+
const endTime = ContentfulService.getField(entry, f.endTime);
|
|
64635
|
+
if (endTime !== void 0) values.endTime = endTime;
|
|
64636
|
+
const regCloseRaw = ContentfulService.getField(entry, f.registrationClosesAt);
|
|
64637
|
+
if (regCloseRaw) {
|
|
64638
|
+
const ms = Date.parse(regCloseRaw);
|
|
64639
|
+
if (!Number.isNaN(ms)) values.registrationClosesAt = Math.floor(ms / 1e3);
|
|
64640
|
+
}
|
|
64641
|
+
await db.insert(events).values(values).onConflictDoUpdate({
|
|
64642
|
+
target: events.id,
|
|
64643
|
+
set: values
|
|
64644
|
+
});
|
|
64645
|
+
count2++;
|
|
64646
|
+
} catch (err) {
|
|
64647
|
+
result.errors.push(`Event ${entry.sys.id}: ${err}`);
|
|
64648
|
+
}
|
|
64649
|
+
}
|
|
64650
|
+
return count2;
|
|
64651
|
+
}
|
|
64652
|
+
async function syncFaqs(db, entries, config3) {
|
|
64653
|
+
let count2 = 0;
|
|
64654
|
+
for (const entry of entries) {
|
|
64655
|
+
const cfId = entry.sys.id;
|
|
64656
|
+
const f = config3.fields.faq;
|
|
64657
|
+
const question = ContentfulService.getField(entry, f.question);
|
|
64658
|
+
const answer = ContentfulService.getField(entry, f.answer);
|
|
64659
|
+
if (!question || !answer) continue;
|
|
64660
|
+
const category = ContentfulService.getField(entry, f.category) ?? null;
|
|
64661
|
+
const sortOrder = ContentfulService.getField(entry, f.sortOrder) ?? 0;
|
|
64662
|
+
await db.insert(faqs).values({
|
|
64663
|
+
id: cfId,
|
|
64664
|
+
question,
|
|
64665
|
+
answer,
|
|
64666
|
+
category,
|
|
64667
|
+
sortOrder
|
|
64668
|
+
}).onConflictDoUpdate({
|
|
64669
|
+
target: faqs.id,
|
|
64670
|
+
set: { question, answer, category, sortOrder }
|
|
64671
|
+
});
|
|
64672
|
+
count2++;
|
|
64673
|
+
}
|
|
64674
|
+
return count2;
|
|
64675
|
+
}
|
|
64676
|
+
async function uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key) {
|
|
64677
|
+
const { data, contentType } = await contentful.downloadAsset(assetUrl);
|
|
64678
|
+
const sha = await computeSha256(data);
|
|
64679
|
+
const existing = await bucket.head(r2Key);
|
|
64680
|
+
if (existing?.customMetadata?.sha256 === sha) {
|
|
64681
|
+
return { skipped: true };
|
|
64682
|
+
}
|
|
64683
|
+
await bucket.put(r2Key, data, {
|
|
64684
|
+
httpMetadata: { contentType },
|
|
64685
|
+
customMetadata: {
|
|
64686
|
+
sha256: sha,
|
|
64687
|
+
source: "contentful",
|
|
64688
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
64689
|
+
}
|
|
64690
|
+
});
|
|
64691
|
+
return { skipped: false };
|
|
64692
|
+
}
|
|
64181
64693
|
async function batchRun(items, fn, concurrency = 5) {
|
|
64182
64694
|
const results = [];
|
|
64183
64695
|
for (let i = 0; i < items.length; i += concurrency) {
|
|
@@ -64187,6 +64699,13 @@ async function batchRun(items, fn, concurrency = 5) {
|
|
|
64187
64699
|
}
|
|
64188
64700
|
return results;
|
|
64189
64701
|
}
|
|
64702
|
+
async function computeSha256(data) {
|
|
64703
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
64704
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
64705
|
+
}
|
|
64706
|
+
function slugify2(text2) {
|
|
64707
|
+
return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
64708
|
+
}
|
|
64190
64709
|
async function ensureContentTypes(mgmt, config3 = {}) {
|
|
64191
64710
|
const eventTypeId = config3.eventTypeId ?? "event";
|
|
64192
64711
|
const themeTypeId = config3.themeTypeId ?? "theme";
|
|
@@ -64207,7 +64726,7 @@ async function ensureContentTypes(mgmt, config3 = {}) {
|
|
|
64207
64726
|
{ id: "endTime", name: "End Time", type: "Symbol" },
|
|
64208
64727
|
{ id: "price", name: "Price", type: "Symbol" },
|
|
64209
64728
|
{ id: "image", name: "Image", type: "Link", linkType: "Asset" },
|
|
64210
|
-
{ id: "
|
|
64729
|
+
{ id: "isSpotlight", name: "Spotlight", type: "Boolean" },
|
|
64211
64730
|
{ id: "maxSlots", name: "Max Slots", type: "Integer" },
|
|
64212
64731
|
{ id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
|
|
64213
64732
|
{ id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
|
|
@@ -64292,7 +64811,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
|
|
|
64292
64811
|
const fields = {
|
|
64293
64812
|
title: ContentfulManagement.locale(event.title),
|
|
64294
64813
|
slug: ContentfulManagement.locale(event.slug),
|
|
64295
|
-
|
|
64814
|
+
isSpotlight: ContentfulManagement.locale(event.isSpotlight),
|
|
64296
64815
|
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
64297
64816
|
};
|
|
64298
64817
|
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
@@ -64363,6 +64882,9 @@ siteConfigRoute.get("/", async (c) => {
|
|
|
64363
64882
|
siteName: config3.site_name ?? null,
|
|
64364
64883
|
registrationGloballyOpen: config3.registration_globally_open ?? true,
|
|
64365
64884
|
maintenanceMode: config3.maintenance_mode ?? false,
|
|
64885
|
+
// Prefer the D1-persisted value over the env-var default so that
|
|
64886
|
+
// PATCH /config/cms_mode changes are reflected immediately.
|
|
64887
|
+
cmsMode: config3.cms_mode ?? c.get("cmsMode"),
|
|
64366
64888
|
now: Math.floor(Date.now() / 1e3)
|
|
64367
64889
|
}
|
|
64368
64890
|
});
|
|
@@ -64384,6 +64906,10 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64384
64906
|
var SYNC_LOCK_KEY = "contentful:sync:lock";
|
|
64385
64907
|
var SYNC_LOCK_TTL = 60;
|
|
64386
64908
|
siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
|
|
64909
|
+
const cmsMode = c.get("cmsMode");
|
|
64910
|
+
if (cmsMode === "cloudflare") {
|
|
64911
|
+
throw serviceUnavailable("Contentful sync is not available in Cloudflare-only mode.");
|
|
64912
|
+
}
|
|
64387
64913
|
if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
|
|
64388
64914
|
throw serviceUnavailable("Contentful Management API credentials not configured.");
|
|
64389
64915
|
}
|
|
@@ -64470,7 +64996,9 @@ faqsRoute.post(
|
|
|
64470
64996
|
const cache3 = new CacheService(c.env.KV);
|
|
64471
64997
|
const [created] = await db.insert(faqs).values(body).returning();
|
|
64472
64998
|
await cache3.del(FAQS_KV_KEY);
|
|
64473
|
-
c.
|
|
64999
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65000
|
+
c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
|
|
65001
|
+
}
|
|
64474
65002
|
return c.json({ data: created }, 201);
|
|
64475
65003
|
}
|
|
64476
65004
|
);
|
|
@@ -64483,7 +65011,9 @@ faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64483
65011
|
const [updated] = await db.update(faqs).set({ ...body, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
|
|
64484
65012
|
if (!updated) throw notFound("FAQ");
|
|
64485
65013
|
await cache3.del(FAQS_KV_KEY);
|
|
64486
|
-
c.
|
|
65014
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65015
|
+
c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
|
|
65016
|
+
}
|
|
64487
65017
|
return c.json({ data: updated });
|
|
64488
65018
|
});
|
|
64489
65019
|
faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
@@ -64759,7 +65289,9 @@ themesRoute.post(
|
|
|
64759
65289
|
const db = createDb(c.env.DB);
|
|
64760
65290
|
try {
|
|
64761
65291
|
const [created] = await db.insert(themes).values(body).returning();
|
|
64762
|
-
c.
|
|
65292
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65293
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
|
|
65294
|
+
}
|
|
64763
65295
|
return c.json({ data: created }, 201);
|
|
64764
65296
|
} catch (err) {
|
|
64765
65297
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -64780,7 +65312,9 @@ themesRoute.patch(
|
|
|
64780
65312
|
try {
|
|
64781
65313
|
const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
|
|
64782
65314
|
if (!updated) throw notFound("Theme");
|
|
64783
|
-
c.
|
|
65315
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65316
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
|
|
65317
|
+
}
|
|
64784
65318
|
return c.json({ data: updated });
|
|
64785
65319
|
} catch (err) {
|
|
64786
65320
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -64795,7 +65329,9 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
64795
65329
|
const db = createDb(c.env.DB);
|
|
64796
65330
|
const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
|
|
64797
65331
|
if (!deleted) throw notFound("Theme");
|
|
64798
|
-
c.
|
|
65332
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
65333
|
+
c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
|
|
65334
|
+
}
|
|
64799
65335
|
return c.body(null, 204);
|
|
64800
65336
|
});
|
|
64801
65337
|
|
|
@@ -64859,6 +65395,83 @@ organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) =>
|
|
|
64859
65395
|
return c.body(null, 204);
|
|
64860
65396
|
});
|
|
64861
65397
|
|
|
65398
|
+
// src/routes/contentful-sync.ts
|
|
65399
|
+
var contentfulSyncRoute = new Hono2();
|
|
65400
|
+
contentfulSyncRoute.post(
|
|
65401
|
+
"/trigger",
|
|
65402
|
+
authMiddleware,
|
|
65403
|
+
adminMiddleware,
|
|
65404
|
+
async (c) => {
|
|
65405
|
+
const env2 = c.env;
|
|
65406
|
+
const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
|
|
65407
|
+
if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_ACCESS_TOKEN) {
|
|
65408
|
+
return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful credentials missing" } }, 400);
|
|
65409
|
+
}
|
|
65410
|
+
const db = createDb(env2.DB);
|
|
65411
|
+
const contentful = new ContentfulService(CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
65412
|
+
try {
|
|
65413
|
+
if (CONTENTFUL_MANAGEMENT_TOKEN) {
|
|
65414
|
+
const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
65415
|
+
await ensureContentTypes(mgmt);
|
|
65416
|
+
}
|
|
65417
|
+
c.executionCtx.waitUntil(
|
|
65418
|
+
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))
|
|
65419
|
+
);
|
|
65420
|
+
return c.json({ message: "Contentful sync triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
|
|
65421
|
+
} catch (err) {
|
|
65422
|
+
console.error("[Contentful] Sync trigger failed:", err);
|
|
65423
|
+
return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger sync" } }, 500);
|
|
65424
|
+
}
|
|
65425
|
+
}
|
|
65426
|
+
);
|
|
65427
|
+
contentfulSyncRoute.post(
|
|
65428
|
+
"/push",
|
|
65429
|
+
authMiddleware,
|
|
65430
|
+
adminMiddleware,
|
|
65431
|
+
async (c) => {
|
|
65432
|
+
const env2 = c.env;
|
|
65433
|
+
const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
|
|
65434
|
+
if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
|
|
65435
|
+
return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful management credentials missing" } }, 400);
|
|
65436
|
+
}
|
|
65437
|
+
const db = createDb(env2.DB);
|
|
65438
|
+
const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
65439
|
+
try {
|
|
65440
|
+
c.executionCtx.waitUntil(
|
|
65441
|
+
pushToContentful(db, mgmt, {}, env2.KV).then((r) => console.log("[Contentful] Push complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Push failed:", err))
|
|
65442
|
+
);
|
|
65443
|
+
return c.json({ message: "Contentful push triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
|
|
65444
|
+
} catch (err) {
|
|
65445
|
+
console.error("[Contentful] Push trigger failed:", err);
|
|
65446
|
+
return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger push" } }, 500);
|
|
65447
|
+
}
|
|
65448
|
+
}
|
|
65449
|
+
);
|
|
65450
|
+
contentfulSyncRoute.get(
|
|
65451
|
+
"/status",
|
|
65452
|
+
authMiddleware,
|
|
65453
|
+
adminMiddleware,
|
|
65454
|
+
async (c) => {
|
|
65455
|
+
const env2 = c.env;
|
|
65456
|
+
const db = createDb(env2.DB);
|
|
65457
|
+
const [[{ themes: themes2 }], [{ events: events2 }], [{ faqs: faqs2 }], [{ orgs }]] = await Promise.all([
|
|
65458
|
+
db.all(sql2`SELECT count(*) as themes FROM themes`),
|
|
65459
|
+
db.all(sql2`SELECT count(*) as events FROM events`),
|
|
65460
|
+
db.all(sql2`SELECT count(*) as faqs FROM faqs`),
|
|
65461
|
+
db.all(sql2`SELECT count(*) as orgs FROM organizations`)
|
|
65462
|
+
]);
|
|
65463
|
+
const isConfigured = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_ACCESS_TOKEN;
|
|
65464
|
+
const canPush = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_MANAGEMENT_TOKEN;
|
|
65465
|
+
return c.json({
|
|
65466
|
+
isConfigured,
|
|
65467
|
+
canPush,
|
|
65468
|
+
cmsMode: c.get("cmsMode"),
|
|
65469
|
+
spaceId: env2.CONTENTFUL_SPACE_ID ?? null,
|
|
65470
|
+
totals: { themes: themes2, events: events2, faqs: faqs2, organizations: orgs }
|
|
65471
|
+
});
|
|
65472
|
+
}
|
|
65473
|
+
);
|
|
65474
|
+
|
|
64862
65475
|
// src/app.ts
|
|
64863
65476
|
function createApp(options = {}) {
|
|
64864
65477
|
const app2 = new Hono2();
|
|
@@ -64872,6 +65485,12 @@ function createApp(options = {}) {
|
|
|
64872
65485
|
app2.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
|
|
64873
65486
|
app2.use("*", createPowChallengeMiddleware());
|
|
64874
65487
|
app2.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
|
|
65488
|
+
app2.use("*", async (c, next) => {
|
|
65489
|
+
const overrideRaw = await c.env.KV.get("config:cms_mode").catch(() => null);
|
|
65490
|
+
const override = overrideRaw ? JSON.parse(overrideRaw) : null;
|
|
65491
|
+
c.set("cmsMode", parseCmsMode(override ?? c.env.CMS_MODE));
|
|
65492
|
+
return next();
|
|
65493
|
+
});
|
|
64875
65494
|
app2.on(["POST", "GET"], "/api/auth/*", (c) => {
|
|
64876
65495
|
const auth = createAuth(c.env);
|
|
64877
65496
|
const req = c.req.raw;
|
|
@@ -64883,7 +65502,7 @@ function createApp(options = {}) {
|
|
|
64883
65502
|
return auth.handler(new Request(req.url, {
|
|
64884
65503
|
method: req.method,
|
|
64885
65504
|
headers: newHeaders,
|
|
64886
|
-
body: req.method === "GET" ? null : req.body,
|
|
65505
|
+
body: req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" ? null : req.body,
|
|
64887
65506
|
redirect: req.redirect
|
|
64888
65507
|
}));
|
|
64889
65508
|
}
|
|
@@ -64904,12 +65523,13 @@ function createApp(options = {}) {
|
|
|
64904
65523
|
app2.post(POW_VERIFY_PATH, handlePowVerify);
|
|
64905
65524
|
app2.route("/health", healthRoute);
|
|
64906
65525
|
app2.route("/api/config", siteConfigRoute);
|
|
64907
|
-
app2.route("/api/
|
|
65526
|
+
app2.route("/api/classes", classesRoute);
|
|
64908
65527
|
app2.route("/api/themes", themesRoute);
|
|
64909
65528
|
app2.route("/api/users", usersRoute);
|
|
64910
65529
|
app2.route("/api/organizations", organizationsRoute);
|
|
64911
65530
|
app2.route("/api/faqs", faqsRoute);
|
|
64912
65531
|
app2.route("/api/uploads", uploadsRoute);
|
|
65532
|
+
app2.route("/api/contentful", contentfulSyncRoute);
|
|
64913
65533
|
app2.route("/internal/gforms-webhook", gformsWebhookRoute);
|
|
64914
65534
|
app2.onError(errorHandler2);
|
|
64915
65535
|
app2.notFound(
|
|
@@ -65543,6 +66163,7 @@ var PATCH_STATEMENTS = [
|
|
|
65543
66163
|
`ALTER TABLE "events" ADD COLUMN "class_code" text`,
|
|
65544
66164
|
`ALTER TABLE "events" ADD COLUMN "start_time" text`,
|
|
65545
66165
|
`ALTER TABLE "events" ADD COLUMN "end_time" text`,
|
|
66166
|
+
`ALTER TABLE "events" RENAME COLUMN "is_major" TO "is_spotlight"`,
|
|
65546
66167
|
`CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
|
|
65547
66168
|
];
|
|
65548
66169
|
var CREATE_STATEMENTS = [
|
|
@@ -65646,7 +66267,7 @@ var CREATE_STATEMENTS = [
|
|
|
65646
66267
|
"class_code" text,
|
|
65647
66268
|
"start_time" text,
|
|
65648
66269
|
"end_time" text,
|
|
65649
|
-
"
|
|
66270
|
+
"is_spotlight" integer DEFAULT false NOT NULL,
|
|
65650
66271
|
"max_slots" integer DEFAULT 0 NOT NULL,
|
|
65651
66272
|
"registered_slots" integer DEFAULT 0 NOT NULL,
|
|
65652
66273
|
"gforms_id" text,
|
|
@@ -65708,7 +66329,9 @@ async function ensureDatabase(d1) {
|
|
|
65708
66329
|
try {
|
|
65709
66330
|
await d1.prepare(sql3).run();
|
|
65710
66331
|
} catch (err) {
|
|
65711
|
-
if (err?.message?.includes("duplicate column"))
|
|
66332
|
+
if (err?.message?.includes("duplicate column") || err?.message?.includes("no such column") && err?.message?.includes("is_major")) {
|
|
66333
|
+
continue;
|
|
66334
|
+
}
|
|
65712
66335
|
throw err;
|
|
65713
66336
|
}
|
|
65714
66337
|
}
|