@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/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createKyselyAdapter, getKyselyDatabaseType } from './chunk-FUCJEA2S.js';
|
|
2
2
|
import { createDefu, wildcardMatch, getOrigin, getHost, getProtocol, betterFetch, PACKAGE_VERSION, isDynamicBaseURLConfig, getBaseURL, resolveBaseURL, isRequestLike, defu } from './chunk-IQEWVHLM.js';
|
|
3
3
|
import './chunk-HHNEB7YR.js';
|
|
4
|
-
import { createMiddleware, Hono, HTTPException, tryDecode, decodeURIComponent_, createPowChallengeMiddleware, POW_VERIFY_PATH, handlePowVerify } from './chunk-
|
|
4
|
+
import { createMiddleware, Hono, HTTPException, tryDecode, decodeURIComponent_, createPowChallengeMiddleware, POW_VERIFY_PATH, handlePowVerify } from './chunk-5YYVBPAE.js';
|
|
5
5
|
import { createRandomStringGenerator, createAdapterFactory, withSpan, ATTR_CONTEXT, import_semantic_conventions, ATTR_HOOK_TYPE, ATTR_OPERATION_ID, generateId, safeJSONParse, getAuthTables, initGetModelName, initGetFieldName } from './chunk-GNRL67OU.js';
|
|
6
6
|
import './chunk-EGRHWZRV.js';
|
|
7
7
|
import { APIError2, BASE_ERROR_CODES, kAPIErrorHeaderSymbol, BetterCallError, BetterAuthError, logger, ValidationError, APIError, env, isProduction, shouldPublishLog, isDevelopment, createLogger, isTest, getBooleanEnvVar, ENV, getEnvVar } from './chunk-MNEW2V4T.js';
|
|
@@ -48,13 +48,28 @@ var errorHandler = (err, c) => {
|
|
|
48
48
|
);
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
// src/types.ts
|
|
52
|
+
function parseCmsMode(raw) {
|
|
53
|
+
if (raw === "cloudflare" || raw === "contentful") return raw;
|
|
54
|
+
return "hybrid";
|
|
55
|
+
}
|
|
56
|
+
function shouldPushToContentful(mode) {
|
|
57
|
+
return mode === "hybrid";
|
|
58
|
+
}
|
|
59
|
+
function shouldPullFromContentful(mode) {
|
|
60
|
+
return mode === "contentful" || mode === "hybrid";
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
// node_modules/hono/dist/middleware/cors/index.js
|
|
52
64
|
var cors = (options) => {
|
|
53
|
-
const
|
|
65
|
+
const defaults = {
|
|
54
66
|
origin: "*",
|
|
55
67
|
allowMethods: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"],
|
|
56
68
|
allowHeaders: [],
|
|
57
|
-
exposeHeaders: []
|
|
69
|
+
exposeHeaders: []
|
|
70
|
+
};
|
|
71
|
+
const opts = {
|
|
72
|
+
...defaults,
|
|
58
73
|
...options
|
|
59
74
|
};
|
|
60
75
|
const findAllowOrigin = ((optsOrigin) => {
|
|
@@ -145,9 +160,22 @@ function createCorsMiddleware(allowedOrigins) {
|
|
|
145
160
|
});
|
|
146
161
|
return async (c, next) => {
|
|
147
162
|
const origin = c.req.header("origin");
|
|
163
|
+
if (c.req.path.startsWith("/api/uploads/images")) {
|
|
164
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
165
|
+
c.header("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
166
|
+
if (c.req.method === "OPTIONS") {
|
|
167
|
+
return c.body(null, 204);
|
|
168
|
+
}
|
|
169
|
+
return next();
|
|
170
|
+
}
|
|
148
171
|
if (!c.req.path.startsWith("/health") && !c.req.path.startsWith("/api/auth") && !c.req.path.startsWith("/internal") && origin && !allowedOrigins.includes("*") && !allowedOrigins.includes(origin)) {
|
|
149
172
|
return c.json(
|
|
150
|
-
{
|
|
173
|
+
{
|
|
174
|
+
error: {
|
|
175
|
+
code: "DOMAIN_RESTRICTED",
|
|
176
|
+
message: `Origin ${origin} is not allowed`
|
|
177
|
+
}
|
|
178
|
+
},
|
|
151
179
|
403
|
|
152
180
|
);
|
|
153
181
|
}
|
|
@@ -36330,12 +36358,14 @@ function drizzle(client, config3 = {}) {
|
|
|
36330
36358
|
// src/db/schema/index.ts
|
|
36331
36359
|
var schema_exports = {};
|
|
36332
36360
|
__export(schema_exports, {
|
|
36361
|
+
CONTENTFUL_CONFIG_KEYS: () => CONTENTFUL_CONFIG_KEYS,
|
|
36333
36362
|
authAccount: () => authAccount,
|
|
36334
36363
|
authSession: () => authSession,
|
|
36335
36364
|
authUser: () => authUser,
|
|
36336
36365
|
authVerification: () => authVerification,
|
|
36337
36366
|
bookmarks: () => bookmarks,
|
|
36338
36367
|
bookmarksRelations: () => bookmarksRelations,
|
|
36368
|
+
contentfulConfig: () => contentfulConfig,
|
|
36339
36369
|
events: () => events,
|
|
36340
36370
|
eventsRelations: () => eventsRelations,
|
|
36341
36371
|
faqs: () => faqs,
|
|
@@ -36387,7 +36417,7 @@ var organizationsRelations = relations(organizations, ({ many }) => ({
|
|
|
36387
36417
|
events: many(events)
|
|
36388
36418
|
}));
|
|
36389
36419
|
|
|
36390
|
-
// src/db/schema/
|
|
36420
|
+
// src/db/schema/classes.ts
|
|
36391
36421
|
var events = sqliteTable(
|
|
36392
36422
|
"events",
|
|
36393
36423
|
{
|
|
@@ -36412,7 +36442,7 @@ var events = sqliteTable(
|
|
|
36412
36442
|
// start time string
|
|
36413
36443
|
endTime: text("end_time"),
|
|
36414
36444
|
// end time string
|
|
36415
|
-
|
|
36445
|
+
isSpotlight: integer2("is_spotlight", { mode: "boolean" }).notNull().default(false),
|
|
36416
36446
|
// Slot tracking (local counter — NOT polled from Google Forms)
|
|
36417
36447
|
maxSlots: integer2("max_slots").notNull().default(0),
|
|
36418
36448
|
registeredSlots: integer2("registered_slots").notNull().default(0),
|
|
@@ -36469,6 +36499,18 @@ var siteConfig = sqliteTable("site_config", {
|
|
|
36469
36499
|
// JSON-serializable string
|
|
36470
36500
|
updatedAt: integer2("updated_at").notNull().default(sql2`(unixepoch())`)
|
|
36471
36501
|
});
|
|
36502
|
+
var CONTENTFUL_CONFIG_KEYS = {
|
|
36503
|
+
ENABLED: "contentful.enabled",
|
|
36504
|
+
SPACE_ID: "contentful.spaceId",
|
|
36505
|
+
MANAGEMENT_TOKEN: "contentful.managementToken",
|
|
36506
|
+
DEFAULT_SPACE_ID: "dlsu-events"
|
|
36507
|
+
};
|
|
36508
|
+
var contentfulConfig = sqliteTable("contentful_config", {
|
|
36509
|
+
space_id: text("space_id"),
|
|
36510
|
+
contentful_enabled: integer2("contentful_enabled").notNull().default(0),
|
|
36511
|
+
last_sync_at: integer2("last_sync_at"),
|
|
36512
|
+
updated_at: integer2("updated_at").default(sql2`(unixepoch())`).notNull()
|
|
36513
|
+
});
|
|
36472
36514
|
|
|
36473
36515
|
// src/db/schema/faqs.ts
|
|
36474
36516
|
var faqs = sqliteTable("faqs", {
|
|
@@ -36638,11 +36680,21 @@ function createAuth(env2) {
|
|
|
36638
36680
|
};
|
|
36639
36681
|
if (isFirstUser) {
|
|
36640
36682
|
await db.insert(users).values(base).onConflictDoUpdate({
|
|
36641
|
-
target: users.
|
|
36642
|
-
set: {
|
|
36683
|
+
target: users.email,
|
|
36684
|
+
set: {
|
|
36685
|
+
betterAuthId: user.id,
|
|
36686
|
+
role: "super_admin",
|
|
36687
|
+
name: user.name ?? user.email.split("@")[0]
|
|
36688
|
+
}
|
|
36643
36689
|
});
|
|
36644
36690
|
} else {
|
|
36645
|
-
await db.insert(users).values(base).
|
|
36691
|
+
await db.insert(users).values(base).onConflictDoUpdate({
|
|
36692
|
+
target: users.email,
|
|
36693
|
+
set: {
|
|
36694
|
+
betterAuthId: user.id,
|
|
36695
|
+
name: user.name ?? user.email.split("@")[0]
|
|
36696
|
+
}
|
|
36697
|
+
});
|
|
36646
36698
|
}
|
|
36647
36699
|
}
|
|
36648
36700
|
}
|
|
@@ -36671,7 +36723,13 @@ async function resolveUser(env2, betterAuthUserId, betterAuthUserEmail, betterAu
|
|
|
36671
36723
|
betterAuthId: betterAuthUserId,
|
|
36672
36724
|
email: betterAuthUserEmail,
|
|
36673
36725
|
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
|
|
36674
|
-
}).
|
|
36726
|
+
}).onConflictDoUpdate({
|
|
36727
|
+
target: users.email,
|
|
36728
|
+
set: {
|
|
36729
|
+
betterAuthId: betterAuthUserId,
|
|
36730
|
+
name: betterAuthUserName ?? betterAuthUserEmail.split("@")[0]
|
|
36731
|
+
}
|
|
36732
|
+
}).returning();
|
|
36675
36733
|
dbUser = created;
|
|
36676
36734
|
}
|
|
36677
36735
|
if (!dbUser) throw unauthorized("Failed to resolve user record");
|
|
@@ -37638,7 +37696,7 @@ var adminEventsRateLimit = createRateLimitMiddleware({
|
|
|
37638
37696
|
identifier: "uid"
|
|
37639
37697
|
});
|
|
37640
37698
|
|
|
37641
|
-
// src/routes/
|
|
37699
|
+
// src/routes/classes.ts
|
|
37642
37700
|
var EVENTS_LIST_KV_KEY = "events:list";
|
|
37643
37701
|
var EVENTS_ETAG_KV_KEY = "events:etag";
|
|
37644
37702
|
var EVENTS_LIST_TTL = 300;
|
|
@@ -37654,7 +37712,7 @@ async function pushEventToContentful(env2, event) {
|
|
|
37654
37712
|
const fields = {
|
|
37655
37713
|
title: ContentfulManagement.locale(event.title),
|
|
37656
37714
|
slug: ContentfulManagement.locale(event.slug),
|
|
37657
|
-
|
|
37715
|
+
isSpotlight: ContentfulManagement.locale(event.isSpotlight),
|
|
37658
37716
|
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
37659
37717
|
};
|
|
37660
37718
|
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
@@ -37717,7 +37775,7 @@ var createEventSchema = external_exports.object({
|
|
|
37717
37775
|
startTime: external_exports.string().optional(),
|
|
37718
37776
|
endTime: external_exports.string().optional(),
|
|
37719
37777
|
registrationClosesAt: external_exports.number().optional(),
|
|
37720
|
-
|
|
37778
|
+
isSpotlight: external_exports.boolean().default(false),
|
|
37721
37779
|
maxSlots: external_exports.number().int().min(0).default(0),
|
|
37722
37780
|
gformsId: external_exports.string().optional(),
|
|
37723
37781
|
gformsUrl: external_exports.string().url().optional(),
|
|
@@ -37726,11 +37784,11 @@ var createEventSchema = external_exports.object({
|
|
|
37726
37784
|
contentfulEntryId: external_exports.string().optional(),
|
|
37727
37785
|
status: external_exports.enum(["draft", "queued", "published"]).default("draft")
|
|
37728
37786
|
});
|
|
37729
|
-
var
|
|
37787
|
+
var classesRoute = new Hono();
|
|
37730
37788
|
function generateSlug(title) {
|
|
37731
37789
|
return title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
37732
37790
|
}
|
|
37733
|
-
|
|
37791
|
+
classesRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
|
|
37734
37792
|
const db = createDb(c.env.DB);
|
|
37735
37793
|
const data = await db.query.events.findMany({
|
|
37736
37794
|
with: { theme: true, organization: true },
|
|
@@ -37738,7 +37796,7 @@ eventsRoute.get("/admin", authMiddleware, adminMiddleware, async (c) => {
|
|
|
37738
37796
|
});
|
|
37739
37797
|
return c.json({ data });
|
|
37740
37798
|
});
|
|
37741
|
-
|
|
37799
|
+
classesRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) => {
|
|
37742
37800
|
const body = await c.req.json();
|
|
37743
37801
|
const db = createDb(c.env.DB);
|
|
37744
37802
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -37766,7 +37824,7 @@ eventsRoute.post("/admin/publish", authMiddleware, adminMiddleware, async (c) =>
|
|
|
37766
37824
|
]);
|
|
37767
37825
|
return c.json({ data: { updated: body.ids.length } });
|
|
37768
37826
|
});
|
|
37769
|
-
|
|
37827
|
+
classesRoute.get("/", eventsListRateLimit, async (c) => {
|
|
37770
37828
|
const db = createDb(c.env.DB);
|
|
37771
37829
|
const cache3 = new CacheService(c.env.KV);
|
|
37772
37830
|
const [latest] = await db.select({ max: events.publishedAt }).from(events).where(eq(events.status, "published")).limit(1);
|
|
@@ -37777,7 +37835,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
37777
37835
|
);
|
|
37778
37836
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
37779
37837
|
if (ifNoneMatch === etag) {
|
|
37780
|
-
return c.
|
|
37838
|
+
return c.body(null, 304);
|
|
37781
37839
|
}
|
|
37782
37840
|
const data = await cache3.getOrSet(
|
|
37783
37841
|
EVENTS_LIST_KV_KEY,
|
|
@@ -37801,7 +37859,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
37801
37859
|
startTime: true,
|
|
37802
37860
|
endTime: true,
|
|
37803
37861
|
registrationClosesAt: true,
|
|
37804
|
-
|
|
37862
|
+
isSpotlight: true,
|
|
37805
37863
|
maxSlots: true,
|
|
37806
37864
|
registeredSlots: true,
|
|
37807
37865
|
gformsUrl: true,
|
|
@@ -37818,7 +37876,7 @@ eventsRoute.get("/", eventsListRateLimit, async (c) => {
|
|
|
37818
37876
|
);
|
|
37819
37877
|
return c.json({ data });
|
|
37820
37878
|
});
|
|
37821
|
-
|
|
37879
|
+
classesRoute.get("/:slug", async (c) => {
|
|
37822
37880
|
const { slug } = c.req.param();
|
|
37823
37881
|
const db = createDb(c.env.DB);
|
|
37824
37882
|
const event = await db.query.events.findFirst({
|
|
@@ -37830,7 +37888,7 @@ eventsRoute.get("/:slug", async (c) => {
|
|
|
37830
37888
|
if (!event) throw notFound("Event");
|
|
37831
37889
|
return c.json({ data: event });
|
|
37832
37890
|
});
|
|
37833
|
-
|
|
37891
|
+
classesRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
37834
37892
|
const { slug } = c.req.param();
|
|
37835
37893
|
const db = createDb(c.env.DB);
|
|
37836
37894
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -37840,7 +37898,7 @@ eventsRoute.get("/:slug/slots", eventsSlotsRateLimit, async (c) => {
|
|
|
37840
37898
|
c.header("Cache-Control", "public, max-age=5, stale-while-revalidate=5");
|
|
37841
37899
|
return c.json({ data: info2 });
|
|
37842
37900
|
});
|
|
37843
|
-
|
|
37901
|
+
classesRoute.post(
|
|
37844
37902
|
"/",
|
|
37845
37903
|
authMiddleware,
|
|
37846
37904
|
adminMiddleware,
|
|
@@ -37875,11 +37933,13 @@ eventsRoute.post(
|
|
|
37875
37933
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
37876
37934
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
37877
37935
|
]);
|
|
37878
|
-
c.
|
|
37936
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
37937
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, created));
|
|
37938
|
+
}
|
|
37879
37939
|
return c.json({ data: created }, 201);
|
|
37880
37940
|
}
|
|
37881
37941
|
);
|
|
37882
|
-
|
|
37942
|
+
classesRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
37883
37943
|
const { slug } = c.req.param();
|
|
37884
37944
|
const body = await c.req.json();
|
|
37885
37945
|
const db = createDb(c.env.DB);
|
|
@@ -37894,10 +37954,12 @@ eventsRoute.patch("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
|
37894
37954
|
cache3.del(EVENTS_LIST_KV_KEY),
|
|
37895
37955
|
cache3.del(EVENTS_ETAG_KV_KEY)
|
|
37896
37956
|
]);
|
|
37897
|
-
c.
|
|
37957
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
37958
|
+
c.executionCtx.waitUntil(pushEventToContentful(c.env, updated));
|
|
37959
|
+
}
|
|
37898
37960
|
return c.json({ data: updated });
|
|
37899
37961
|
});
|
|
37900
|
-
|
|
37962
|
+
classesRoute.delete("/:slug", authMiddleware, adminMiddleware, async (c) => {
|
|
37901
37963
|
const { slug } = c.req.param();
|
|
37902
37964
|
const db = createDb(c.env.DB);
|
|
37903
37965
|
const cache3 = new CacheService(c.env.KV);
|
|
@@ -37996,7 +38058,447 @@ usersRoute.delete("/me/bookmarks/:eventId", authMiddleware, async (c) => {
|
|
|
37996
38058
|
return c.json({ data: { bookmarked: false } });
|
|
37997
38059
|
});
|
|
37998
38060
|
|
|
38061
|
+
// src/services/contentful.ts
|
|
38062
|
+
var CONTENTFUL_CDN = "https://cdn.contentful.com";
|
|
38063
|
+
var ContentfulService = class _ContentfulService {
|
|
38064
|
+
spaceId;
|
|
38065
|
+
accessToken;
|
|
38066
|
+
environment;
|
|
38067
|
+
constructor(spaceId, accessToken, environment = "master") {
|
|
38068
|
+
this.spaceId = spaceId;
|
|
38069
|
+
this.accessToken = accessToken;
|
|
38070
|
+
this.environment = environment;
|
|
38071
|
+
}
|
|
38072
|
+
/**
|
|
38073
|
+
* Returns true if the required Contentful credentials are configured.
|
|
38074
|
+
*/
|
|
38075
|
+
static isConfigured(spaceId, accessToken) {
|
|
38076
|
+
return !!(spaceId && accessToken);
|
|
38077
|
+
}
|
|
38078
|
+
// ─── Entries ─────────────────────────────────────────────────────────────
|
|
38079
|
+
/**
|
|
38080
|
+
* Fetch all entries of a given content type.
|
|
38081
|
+
* Handles pagination automatically (100 per page, Contentful max).
|
|
38082
|
+
*/
|
|
38083
|
+
async getEntries(contentTypeId) {
|
|
38084
|
+
const allItems = [];
|
|
38085
|
+
let skip = 0;
|
|
38086
|
+
const limit = 100;
|
|
38087
|
+
do {
|
|
38088
|
+
const url2 = this.buildUrl(`/entries`, {
|
|
38089
|
+
content_type: contentTypeId,
|
|
38090
|
+
skip: String(skip),
|
|
38091
|
+
limit: String(limit),
|
|
38092
|
+
include: "2"
|
|
38093
|
+
// resolve up to 2 levels of linked entries/assets
|
|
38094
|
+
});
|
|
38095
|
+
const res = await fetch(url2, { headers: this.headers() });
|
|
38096
|
+
if (!res.ok) {
|
|
38097
|
+
throw new Error(`Contentful entries error: ${res.status} ${await res.text()}`);
|
|
38098
|
+
}
|
|
38099
|
+
const data = await res.json();
|
|
38100
|
+
allItems.push(...data.items);
|
|
38101
|
+
skip += limit;
|
|
38102
|
+
if (allItems.length >= data.total) break;
|
|
38103
|
+
} while (true);
|
|
38104
|
+
return allItems;
|
|
38105
|
+
}
|
|
38106
|
+
/**
|
|
38107
|
+
* Fetch all assets. Handles pagination.
|
|
38108
|
+
*/
|
|
38109
|
+
async getAssets() {
|
|
38110
|
+
const allItems = [];
|
|
38111
|
+
let skip = 0;
|
|
38112
|
+
const limit = 100;
|
|
38113
|
+
do {
|
|
38114
|
+
const url2 = this.buildUrl(`/assets`, {
|
|
38115
|
+
skip: String(skip),
|
|
38116
|
+
limit: String(limit)
|
|
38117
|
+
});
|
|
38118
|
+
const res = await fetch(url2, { headers: this.headers() });
|
|
38119
|
+
if (!res.ok) {
|
|
38120
|
+
throw new Error(`Contentful assets error: ${res.status} ${await res.text()}`);
|
|
38121
|
+
}
|
|
38122
|
+
const data = await res.json();
|
|
38123
|
+
allItems.push(...data.items);
|
|
38124
|
+
skip += limit;
|
|
38125
|
+
if (allItems.length >= data.total) break;
|
|
38126
|
+
} while (true);
|
|
38127
|
+
return allItems;
|
|
38128
|
+
}
|
|
38129
|
+
// ─── Asset file download ─────────────────────────────────────────────────
|
|
38130
|
+
/**
|
|
38131
|
+
* Download an asset file from Contentful's CDN.
|
|
38132
|
+
* Returns the raw ArrayBuffer and content type.
|
|
38133
|
+
*/
|
|
38134
|
+
async downloadAsset(assetUrl) {
|
|
38135
|
+
const url2 = assetUrl.startsWith("//") ? `https:${assetUrl}` : assetUrl;
|
|
38136
|
+
const res = await fetch(url2);
|
|
38137
|
+
if (!res.ok) {
|
|
38138
|
+
throw new Error(`Failed to download asset: ${res.status}`);
|
|
38139
|
+
}
|
|
38140
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
38141
|
+
const data = await res.arrayBuffer();
|
|
38142
|
+
return { data, contentType };
|
|
38143
|
+
}
|
|
38144
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
38145
|
+
/**
|
|
38146
|
+
* Extract a field value from a Contentful entry, handling locale wrapping.
|
|
38147
|
+
* Contentful fields are often `{ "en-US": value }` — this unwraps them.
|
|
38148
|
+
*/
|
|
38149
|
+
static getField(entry, fieldName) {
|
|
38150
|
+
const raw = entry.fields[fieldName];
|
|
38151
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
38152
|
+
if (typeof raw === "object" && !Array.isArray(raw) && "en-US" in raw) {
|
|
38153
|
+
return raw["en-US"];
|
|
38154
|
+
}
|
|
38155
|
+
return raw;
|
|
38156
|
+
}
|
|
38157
|
+
/**
|
|
38158
|
+
* Extract a linked entry/sys reference ID from a reference field.
|
|
38159
|
+
*/
|
|
38160
|
+
static getRefId(entry, fieldName) {
|
|
38161
|
+
const ref = _ContentfulService.getField(entry, fieldName);
|
|
38162
|
+
return ref?.sys?.id;
|
|
38163
|
+
}
|
|
38164
|
+
/**
|
|
38165
|
+
* Extract an asset URL from a linked asset field.
|
|
38166
|
+
*/
|
|
38167
|
+
static getAssetUrl(entry, fieldName) {
|
|
38168
|
+
const asset = _ContentfulService.getField(entry, fieldName);
|
|
38169
|
+
return asset?.sys?.id;
|
|
38170
|
+
}
|
|
38171
|
+
/**
|
|
38172
|
+
* Resolve an asset URL by ID from a list of fetched assets.
|
|
38173
|
+
*/
|
|
38174
|
+
static resolveAssetUrl(assets, assetId) {
|
|
38175
|
+
const asset = assets.find((a) => a.sys.id === assetId);
|
|
38176
|
+
return asset?.fields?.file?.url;
|
|
38177
|
+
}
|
|
38178
|
+
// ─── Private ─────────────────────────────────────────────────────────────
|
|
38179
|
+
headers() {
|
|
38180
|
+
return {
|
|
38181
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
38182
|
+
"Content-Type": "application/json"
|
|
38183
|
+
};
|
|
38184
|
+
}
|
|
38185
|
+
buildUrl(path, params = {}) {
|
|
38186
|
+
const url2 = new URL(
|
|
38187
|
+
`/spaces/${this.spaceId}/environments/${this.environment}${path}`,
|
|
38188
|
+
CONTENTFUL_CDN
|
|
38189
|
+
);
|
|
38190
|
+
for (const [key, value] of Object.entries(params)) {
|
|
38191
|
+
url2.searchParams.set(key, value);
|
|
38192
|
+
}
|
|
38193
|
+
return url2.toString();
|
|
38194
|
+
}
|
|
38195
|
+
};
|
|
38196
|
+
|
|
37999
38197
|
// src/services/snapshot.ts
|
|
38198
|
+
var DEFAULT_FIELDS = {
|
|
38199
|
+
event: {
|
|
38200
|
+
title: "title",
|
|
38201
|
+
slug: "slug",
|
|
38202
|
+
theme: "theme",
|
|
38203
|
+
organization: "organization",
|
|
38204
|
+
venue: "venue",
|
|
38205
|
+
date: "date",
|
|
38206
|
+
startTime: "startTime",
|
|
38207
|
+
endTime: "endTime",
|
|
38208
|
+
price: "price",
|
|
38209
|
+
image: "image",
|
|
38210
|
+
isSpotlight: "isSpotlight",
|
|
38211
|
+
maxSlots: "maxSlots",
|
|
38212
|
+
gformsUrl: "gformsUrl",
|
|
38213
|
+
gformsEditorUrl: "gformsEditorUrl",
|
|
38214
|
+
registrationClosesAt: "registrationClosesAt",
|
|
38215
|
+
classCode: "classCode"
|
|
38216
|
+
},
|
|
38217
|
+
theme: {
|
|
38218
|
+
name: "name",
|
|
38219
|
+
path: "path"
|
|
38220
|
+
},
|
|
38221
|
+
faq: {
|
|
38222
|
+
question: "question",
|
|
38223
|
+
answer: "answer",
|
|
38224
|
+
category: "category",
|
|
38225
|
+
sortOrder: "sortOrder"
|
|
38226
|
+
},
|
|
38227
|
+
organization: {
|
|
38228
|
+
name: "name",
|
|
38229
|
+
acronym: "acronym",
|
|
38230
|
+
logoUrl: "logoUrl",
|
|
38231
|
+
link: "link"
|
|
38232
|
+
},
|
|
38233
|
+
siteConfig: {
|
|
38234
|
+
key: "key",
|
|
38235
|
+
value: "value"
|
|
38236
|
+
}
|
|
38237
|
+
};
|
|
38238
|
+
var CONTENTFUL_CACHE_PREFIX = "contentful:cache";
|
|
38239
|
+
var CONTENTFUL_CACHE_TTL = 300;
|
|
38240
|
+
async function snapshotAllContent(db, bucket, contentful, config3 = {}, kv) {
|
|
38241
|
+
const mergedConfig = {
|
|
38242
|
+
eventTypeId: config3.eventTypeId ?? "event",
|
|
38243
|
+
themeTypeId: config3.themeTypeId ?? "theme",
|
|
38244
|
+
faqTypeId: config3.faqTypeId ?? "faq",
|
|
38245
|
+
organizationTypeId: config3.organizationTypeId ?? "organization",
|
|
38246
|
+
fields: {
|
|
38247
|
+
event: { ...DEFAULT_FIELDS.event, ...config3.fields?.event },
|
|
38248
|
+
theme: { ...DEFAULT_FIELDS.theme, ...config3.fields?.theme },
|
|
38249
|
+
faq: { ...DEFAULT_FIELDS.faq, ...config3.fields?.faq },
|
|
38250
|
+
organization: { ...DEFAULT_FIELDS.organization, ...config3.fields?.organization },
|
|
38251
|
+
siteConfig: { ...DEFAULT_FIELDS.siteConfig, ...config3.fields?.siteConfig }
|
|
38252
|
+
}
|
|
38253
|
+
};
|
|
38254
|
+
const result = {
|
|
38255
|
+
themesSynced: 0,
|
|
38256
|
+
eventsSynced: 0,
|
|
38257
|
+
faqsSynced: 0,
|
|
38258
|
+
organizationsSynced: 0,
|
|
38259
|
+
imagesUploaded: 0,
|
|
38260
|
+
imagesSkipped: 0,
|
|
38261
|
+
errors: []
|
|
38262
|
+
};
|
|
38263
|
+
let allAssets = [];
|
|
38264
|
+
if (bucket) {
|
|
38265
|
+
try {
|
|
38266
|
+
allAssets = await contentful.getAssets();
|
|
38267
|
+
} catch (err) {
|
|
38268
|
+
result.errors.push(`Failed to fetch assets: ${err}`);
|
|
38269
|
+
}
|
|
38270
|
+
}
|
|
38271
|
+
try {
|
|
38272
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:themes`;
|
|
38273
|
+
let themeEntries;
|
|
38274
|
+
if (kv) {
|
|
38275
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
38276
|
+
if (cached2) {
|
|
38277
|
+
themeEntries = cached2;
|
|
38278
|
+
} else {
|
|
38279
|
+
themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
|
|
38280
|
+
await kv.put(cacheKey, JSON.stringify(themeEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
38281
|
+
}
|
|
38282
|
+
} else {
|
|
38283
|
+
themeEntries = await contentful.getEntries(mergedConfig.themeTypeId);
|
|
38284
|
+
}
|
|
38285
|
+
result.themesSynced = await syncThemes(db, themeEntries, mergedConfig);
|
|
38286
|
+
} catch (err) {
|
|
38287
|
+
result.errors.push(`Themes sync failed: ${err}`);
|
|
38288
|
+
}
|
|
38289
|
+
try {
|
|
38290
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:organizations`;
|
|
38291
|
+
let orgEntries;
|
|
38292
|
+
if (kv) {
|
|
38293
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
38294
|
+
if (cached2) {
|
|
38295
|
+
orgEntries = cached2;
|
|
38296
|
+
} else {
|
|
38297
|
+
orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
|
|
38298
|
+
await kv.put(cacheKey, JSON.stringify(orgEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
38299
|
+
}
|
|
38300
|
+
} else {
|
|
38301
|
+
orgEntries = await contentful.getEntries(mergedConfig.organizationTypeId);
|
|
38302
|
+
}
|
|
38303
|
+
await syncOrganizations(db, orgEntries, mergedConfig);
|
|
38304
|
+
} catch (err) {
|
|
38305
|
+
result.errors.push(`Organizations sync failed: ${err}`);
|
|
38306
|
+
}
|
|
38307
|
+
try {
|
|
38308
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:events`;
|
|
38309
|
+
let eventEntries;
|
|
38310
|
+
if (kv) {
|
|
38311
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
38312
|
+
if (cached2) {
|
|
38313
|
+
eventEntries = cached2;
|
|
38314
|
+
} else {
|
|
38315
|
+
eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
|
|
38316
|
+
await kv.put(cacheKey, JSON.stringify(eventEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
38317
|
+
}
|
|
38318
|
+
} else {
|
|
38319
|
+
eventEntries = await contentful.getEntries(mergedConfig.eventTypeId);
|
|
38320
|
+
}
|
|
38321
|
+
result.eventsSynced = await syncEvents(db, bucket, contentful, allAssets, eventEntries, mergedConfig, result);
|
|
38322
|
+
} catch (err) {
|
|
38323
|
+
result.errors.push(`Events sync failed: ${err}`);
|
|
38324
|
+
}
|
|
38325
|
+
try {
|
|
38326
|
+
const cacheKey = `${CONTENTFUL_CACHE_PREFIX}:faqs`;
|
|
38327
|
+
let faqEntries;
|
|
38328
|
+
if (kv) {
|
|
38329
|
+
const cached2 = await kv.get(cacheKey, "json");
|
|
38330
|
+
if (cached2) {
|
|
38331
|
+
faqEntries = cached2;
|
|
38332
|
+
} else {
|
|
38333
|
+
faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
|
|
38334
|
+
await kv.put(cacheKey, JSON.stringify(faqEntries), { expirationTtl: CONTENTFUL_CACHE_TTL });
|
|
38335
|
+
}
|
|
38336
|
+
} else {
|
|
38337
|
+
faqEntries = await contentful.getEntries(mergedConfig.faqTypeId);
|
|
38338
|
+
}
|
|
38339
|
+
result.faqsSynced = await syncFaqs(db, faqEntries, mergedConfig);
|
|
38340
|
+
} catch (err) {
|
|
38341
|
+
result.errors.push(`FAQs sync failed: ${err}`);
|
|
38342
|
+
}
|
|
38343
|
+
console.log(
|
|
38344
|
+
`[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`
|
|
38345
|
+
);
|
|
38346
|
+
return result;
|
|
38347
|
+
}
|
|
38348
|
+
async function syncThemes(db, entries, config3) {
|
|
38349
|
+
let count2 = 0;
|
|
38350
|
+
for (const entry of entries) {
|
|
38351
|
+
const cfId = entry.sys.id;
|
|
38352
|
+
const name = ContentfulService.getField(entry, config3.fields.theme.name);
|
|
38353
|
+
const path = ContentfulService.getField(entry, config3.fields.theme.path);
|
|
38354
|
+
if (!name || !path) continue;
|
|
38355
|
+
await db.insert(themes).values({ id: cfId, name, path }).onConflictDoUpdate({
|
|
38356
|
+
target: themes.id,
|
|
38357
|
+
set: { name, path }
|
|
38358
|
+
});
|
|
38359
|
+
count2++;
|
|
38360
|
+
}
|
|
38361
|
+
return count2;
|
|
38362
|
+
}
|
|
38363
|
+
async function syncOrganizations(db, entries, config3) {
|
|
38364
|
+
let count2 = 0;
|
|
38365
|
+
for (const entry of entries) {
|
|
38366
|
+
const cfId = entry.sys.id;
|
|
38367
|
+
const f = config3.fields.organization;
|
|
38368
|
+
const name = ContentfulService.getField(entry, f.name);
|
|
38369
|
+
const acronym = ContentfulService.getField(entry, f.acronym);
|
|
38370
|
+
if (!name || !acronym) continue;
|
|
38371
|
+
const logoUrl = ContentfulService.getField(entry, f.logoUrl) ?? null;
|
|
38372
|
+
const link = ContentfulService.getField(entry, f.link) ?? null;
|
|
38373
|
+
await db.insert(organizations).values({ id: cfId, name, acronym, logoUrl, link }).onConflictDoUpdate({
|
|
38374
|
+
target: organizations.id,
|
|
38375
|
+
set: { name, acronym, logoUrl, link }
|
|
38376
|
+
});
|
|
38377
|
+
count2++;
|
|
38378
|
+
}
|
|
38379
|
+
return count2;
|
|
38380
|
+
}
|
|
38381
|
+
async function syncEvents(db, bucket, contentful, allAssets, entries, config3, result) {
|
|
38382
|
+
let count2 = 0;
|
|
38383
|
+
for (const entry of entries) {
|
|
38384
|
+
try {
|
|
38385
|
+
const cfId = entry.sys.id;
|
|
38386
|
+
const f = config3.fields.event;
|
|
38387
|
+
const title = ContentfulService.getField(entry, f.title);
|
|
38388
|
+
if (!title) {
|
|
38389
|
+
result.errors.push(`Event ${cfId}: missing title, skipping`);
|
|
38390
|
+
continue;
|
|
38391
|
+
}
|
|
38392
|
+
const slug = ContentfulService.getField(entry, f.slug) ?? slugify2(title);
|
|
38393
|
+
const themeRef = ContentfulService.getField(entry, f.theme);
|
|
38394
|
+
const themeId = themeRef?.sys?.id ?? null;
|
|
38395
|
+
const orgRef = ContentfulService.getField(entry, f.organization);
|
|
38396
|
+
const organizationId = orgRef?.sys?.id ?? null;
|
|
38397
|
+
let backgroundImageUrl = null;
|
|
38398
|
+
if (bucket) {
|
|
38399
|
+
const imageRef = ContentfulService.getField(entry, f.image);
|
|
38400
|
+
const assetId = imageRef?.sys?.id;
|
|
38401
|
+
if (assetId) {
|
|
38402
|
+
const assetUrl = ContentfulService.resolveAssetUrl(allAssets, assetId);
|
|
38403
|
+
if (assetUrl) {
|
|
38404
|
+
const r2Key = `contentful/${assetId}`;
|
|
38405
|
+
const uploaded = await uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key);
|
|
38406
|
+
if (uploaded.skipped) {
|
|
38407
|
+
result.imagesSkipped++;
|
|
38408
|
+
} else {
|
|
38409
|
+
result.imagesUploaded++;
|
|
38410
|
+
}
|
|
38411
|
+
backgroundImageUrl = `/uploads/images/${r2Key}`;
|
|
38412
|
+
}
|
|
38413
|
+
}
|
|
38414
|
+
}
|
|
38415
|
+
const values = {
|
|
38416
|
+
id: cfId,
|
|
38417
|
+
contentfulEntryId: cfId,
|
|
38418
|
+
title,
|
|
38419
|
+
slug,
|
|
38420
|
+
themeId,
|
|
38421
|
+
organizationId,
|
|
38422
|
+
updatedAt: entry.sys.updatedAt
|
|
38423
|
+
};
|
|
38424
|
+
const venue = ContentfulService.getField(entry, f.venue);
|
|
38425
|
+
if (venue !== void 0) values.venue = venue;
|
|
38426
|
+
const date5 = ContentfulService.getField(entry, f.date);
|
|
38427
|
+
if (date5 !== void 0) values.dateTime = date5;
|
|
38428
|
+
const price = ContentfulService.getField(entry, f.price);
|
|
38429
|
+
if (price !== void 0) values.price = price;
|
|
38430
|
+
if (backgroundImageUrl) values.backgroundImageUrl = backgroundImageUrl;
|
|
38431
|
+
const isSpotlight = ContentfulService.getField(entry, f.isSpotlight);
|
|
38432
|
+
if (isSpotlight !== void 0) values.isSpotlight = isSpotlight;
|
|
38433
|
+
const maxSlots = ContentfulService.getField(entry, f.maxSlots);
|
|
38434
|
+
if (maxSlots !== void 0) values.maxSlots = maxSlots;
|
|
38435
|
+
const gformsUrl = ContentfulService.getField(entry, f.gformsUrl);
|
|
38436
|
+
if (gformsUrl !== void 0) values.gformsUrl = gformsUrl;
|
|
38437
|
+
const gformsEditorUrl = ContentfulService.getField(entry, f.gformsEditorUrl);
|
|
38438
|
+
if (gformsEditorUrl !== void 0) values.gformsEditorUrl = gformsEditorUrl;
|
|
38439
|
+
const classCode = ContentfulService.getField(entry, f.classCode);
|
|
38440
|
+
if (classCode !== void 0) values.classCode = classCode;
|
|
38441
|
+
const startTime = ContentfulService.getField(entry, f.startTime);
|
|
38442
|
+
if (startTime !== void 0) values.startTime = startTime;
|
|
38443
|
+
const endTime = ContentfulService.getField(entry, f.endTime);
|
|
38444
|
+
if (endTime !== void 0) values.endTime = endTime;
|
|
38445
|
+
const regCloseRaw = ContentfulService.getField(entry, f.registrationClosesAt);
|
|
38446
|
+
if (regCloseRaw) {
|
|
38447
|
+
const ms = Date.parse(regCloseRaw);
|
|
38448
|
+
if (!Number.isNaN(ms)) values.registrationClosesAt = Math.floor(ms / 1e3);
|
|
38449
|
+
}
|
|
38450
|
+
await db.insert(events).values(values).onConflictDoUpdate({
|
|
38451
|
+
target: events.id,
|
|
38452
|
+
set: values
|
|
38453
|
+
});
|
|
38454
|
+
count2++;
|
|
38455
|
+
} catch (err) {
|
|
38456
|
+
result.errors.push(`Event ${entry.sys.id}: ${err}`);
|
|
38457
|
+
}
|
|
38458
|
+
}
|
|
38459
|
+
return count2;
|
|
38460
|
+
}
|
|
38461
|
+
async function syncFaqs(db, entries, config3) {
|
|
38462
|
+
let count2 = 0;
|
|
38463
|
+
for (const entry of entries) {
|
|
38464
|
+
const cfId = entry.sys.id;
|
|
38465
|
+
const f = config3.fields.faq;
|
|
38466
|
+
const question = ContentfulService.getField(entry, f.question);
|
|
38467
|
+
const answer = ContentfulService.getField(entry, f.answer);
|
|
38468
|
+
if (!question || !answer) continue;
|
|
38469
|
+
const category = ContentfulService.getField(entry, f.category) ?? null;
|
|
38470
|
+
const sortOrder = ContentfulService.getField(entry, f.sortOrder) ?? 0;
|
|
38471
|
+
await db.insert(faqs).values({
|
|
38472
|
+
id: cfId,
|
|
38473
|
+
question,
|
|
38474
|
+
answer,
|
|
38475
|
+
category,
|
|
38476
|
+
sortOrder
|
|
38477
|
+
}).onConflictDoUpdate({
|
|
38478
|
+
target: faqs.id,
|
|
38479
|
+
set: { question, answer, category, sortOrder }
|
|
38480
|
+
});
|
|
38481
|
+
count2++;
|
|
38482
|
+
}
|
|
38483
|
+
return count2;
|
|
38484
|
+
}
|
|
38485
|
+
async function uploadAssetIfChanged(bucket, contentful, assetUrl, r2Key) {
|
|
38486
|
+
const { data, contentType } = await contentful.downloadAsset(assetUrl);
|
|
38487
|
+
const sha = await computeSha256(data);
|
|
38488
|
+
const existing = await bucket.head(r2Key);
|
|
38489
|
+
if (existing?.customMetadata?.sha256 === sha) {
|
|
38490
|
+
return { skipped: true };
|
|
38491
|
+
}
|
|
38492
|
+
await bucket.put(r2Key, data, {
|
|
38493
|
+
httpMetadata: { contentType },
|
|
38494
|
+
customMetadata: {
|
|
38495
|
+
sha256: sha,
|
|
38496
|
+
source: "contentful",
|
|
38497
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38498
|
+
}
|
|
38499
|
+
});
|
|
38500
|
+
return { skipped: false };
|
|
38501
|
+
}
|
|
38000
38502
|
async function batchRun(items, fn, concurrency = 5) {
|
|
38001
38503
|
const results = [];
|
|
38002
38504
|
for (let i = 0; i < items.length; i += concurrency) {
|
|
@@ -38006,6 +38508,13 @@ async function batchRun(items, fn, concurrency = 5) {
|
|
|
38006
38508
|
}
|
|
38007
38509
|
return results;
|
|
38008
38510
|
}
|
|
38511
|
+
async function computeSha256(data) {
|
|
38512
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
38513
|
+
return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
38514
|
+
}
|
|
38515
|
+
function slugify2(text2) {
|
|
38516
|
+
return text2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
38517
|
+
}
|
|
38009
38518
|
async function ensureContentTypes(mgmt, config3 = {}) {
|
|
38010
38519
|
const eventTypeId = config3.eventTypeId ?? "event";
|
|
38011
38520
|
const themeTypeId = config3.themeTypeId ?? "theme";
|
|
@@ -38026,7 +38535,7 @@ async function ensureContentTypes(mgmt, config3 = {}) {
|
|
|
38026
38535
|
{ id: "endTime", name: "End Time", type: "Symbol" },
|
|
38027
38536
|
{ id: "price", name: "Price", type: "Symbol" },
|
|
38028
38537
|
{ id: "image", name: "Image", type: "Link", linkType: "Asset" },
|
|
38029
|
-
{ id: "
|
|
38538
|
+
{ id: "isSpotlight", name: "Spotlight", type: "Boolean" },
|
|
38030
38539
|
{ id: "maxSlots", name: "Max Slots", type: "Integer" },
|
|
38031
38540
|
{ id: "gformsUrl", name: "Google Forms URL", type: "Symbol" },
|
|
38032
38541
|
{ id: "gformsEditorUrl", name: "Google Forms Editor URL", type: "Symbol" },
|
|
@@ -38111,7 +38620,7 @@ async function pushToContentful(db, mgmt, config3 = {}, kv, forceFull = false) {
|
|
|
38111
38620
|
const fields = {
|
|
38112
38621
|
title: ContentfulManagement.locale(event.title),
|
|
38113
38622
|
slug: ContentfulManagement.locale(event.slug),
|
|
38114
|
-
|
|
38623
|
+
isSpotlight: ContentfulManagement.locale(event.isSpotlight),
|
|
38115
38624
|
maxSlots: ContentfulManagement.locale(event.maxSlots)
|
|
38116
38625
|
};
|
|
38117
38626
|
if (event.themeId) fields.theme = ContentfulManagement.entryRef(event.themeId);
|
|
@@ -38182,6 +38691,9 @@ siteConfigRoute.get("/", async (c) => {
|
|
|
38182
38691
|
siteName: config3.site_name ?? null,
|
|
38183
38692
|
registrationGloballyOpen: config3.registration_globally_open ?? true,
|
|
38184
38693
|
maintenanceMode: config3.maintenance_mode ?? false,
|
|
38694
|
+
// Prefer the D1-persisted value over the env-var default so that
|
|
38695
|
+
// PATCH /config/cms_mode changes are reflected immediately.
|
|
38696
|
+
cmsMode: config3.cms_mode ?? c.get("cmsMode"),
|
|
38185
38697
|
now: Math.floor(Date.now() / 1e3)
|
|
38186
38698
|
}
|
|
38187
38699
|
});
|
|
@@ -38203,6 +38715,10 @@ siteConfigRoute.patch("/:key", authMiddleware, adminMiddleware, async (c) => {
|
|
|
38203
38715
|
var SYNC_LOCK_KEY = "contentful:sync:lock";
|
|
38204
38716
|
var SYNC_LOCK_TTL = 60;
|
|
38205
38717
|
siteConfigRoute.post("/sync-content", authMiddleware, adminMiddleware, async (c) => {
|
|
38718
|
+
const cmsMode = c.get("cmsMode");
|
|
38719
|
+
if (cmsMode === "cloudflare") {
|
|
38720
|
+
throw serviceUnavailable("Contentful sync is not available in Cloudflare-only mode.");
|
|
38721
|
+
}
|
|
38206
38722
|
if (!ContentfulManagement.isConfigured(c.env.CONTENTFUL_SPACE_ID, c.env.CONTENTFUL_MANAGEMENT_TOKEN)) {
|
|
38207
38723
|
throw serviceUnavailable("Contentful Management API credentials not configured.");
|
|
38208
38724
|
}
|
|
@@ -38289,7 +38805,9 @@ faqsRoute.post(
|
|
|
38289
38805
|
const cache3 = new CacheService(c.env.KV);
|
|
38290
38806
|
const [created] = await db.insert(faqs).values(body).returning();
|
|
38291
38807
|
await cache3.del(FAQS_KV_KEY);
|
|
38292
|
-
c.
|
|
38808
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
38809
|
+
c.executionCtx.waitUntil(pushFaqToContentful(c.env, created));
|
|
38810
|
+
}
|
|
38293
38811
|
return c.json({ data: created }, 201);
|
|
38294
38812
|
}
|
|
38295
38813
|
);
|
|
@@ -38302,7 +38820,9 @@ faqsRoute.patch("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
38302
38820
|
const [updated] = await db.update(faqs).set({ ...body, updatedAt: now2 }).where(eq(faqs.id, id)).returning();
|
|
38303
38821
|
if (!updated) throw notFound("FAQ");
|
|
38304
38822
|
await cache3.del(FAQS_KV_KEY);
|
|
38305
|
-
c.
|
|
38823
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
38824
|
+
c.executionCtx.waitUntil(pushFaqToContentful(c.env, updated));
|
|
38825
|
+
}
|
|
38306
38826
|
return c.json({ data: updated });
|
|
38307
38827
|
});
|
|
38308
38828
|
faqsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
@@ -38578,7 +39098,9 @@ themesRoute.post(
|
|
|
38578
39098
|
const db = createDb(c.env.DB);
|
|
38579
39099
|
try {
|
|
38580
39100
|
const [created] = await db.insert(themes).values(body).returning();
|
|
38581
|
-
c.
|
|
39101
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
39102
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, created));
|
|
39103
|
+
}
|
|
38582
39104
|
return c.json({ data: created }, 201);
|
|
38583
39105
|
} catch (err) {
|
|
38584
39106
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -38599,7 +39121,9 @@ themesRoute.patch(
|
|
|
38599
39121
|
try {
|
|
38600
39122
|
const [updated] = await db.update(themes).set(body).where(eq(themes.id, id)).returning();
|
|
38601
39123
|
if (!updated) throw notFound("Theme");
|
|
38602
|
-
c.
|
|
39124
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
39125
|
+
c.executionCtx.waitUntil(pushThemeToContentful(c.env, updated));
|
|
39126
|
+
}
|
|
38603
39127
|
return c.json({ data: updated });
|
|
38604
39128
|
} catch (err) {
|
|
38605
39129
|
if (err.message && err.message.includes("UNIQUE constraint failed")) {
|
|
@@ -38614,7 +39138,9 @@ themesRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) => {
|
|
|
38614
39138
|
const db = createDb(c.env.DB);
|
|
38615
39139
|
const [deleted] = await db.delete(themes).where(eq(themes.id, id)).returning();
|
|
38616
39140
|
if (!deleted) throw notFound("Theme");
|
|
38617
|
-
c.
|
|
39141
|
+
if (c.get("cmsMode") === "hybrid") {
|
|
39142
|
+
c.executionCtx.waitUntil(deleteThemeFromContentful(c.env, id));
|
|
39143
|
+
}
|
|
38618
39144
|
return c.body(null, 204);
|
|
38619
39145
|
});
|
|
38620
39146
|
|
|
@@ -38678,6 +39204,83 @@ organizationsRoute.delete("/:id", authMiddleware, adminMiddleware, async (c) =>
|
|
|
38678
39204
|
return c.body(null, 204);
|
|
38679
39205
|
});
|
|
38680
39206
|
|
|
39207
|
+
// src/routes/contentful-sync.ts
|
|
39208
|
+
var contentfulSyncRoute = new Hono();
|
|
39209
|
+
contentfulSyncRoute.post(
|
|
39210
|
+
"/trigger",
|
|
39211
|
+
authMiddleware,
|
|
39212
|
+
adminMiddleware,
|
|
39213
|
+
async (c) => {
|
|
39214
|
+
const env2 = c.env;
|
|
39215
|
+
const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
|
|
39216
|
+
if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_ACCESS_TOKEN) {
|
|
39217
|
+
return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful credentials missing" } }, 400);
|
|
39218
|
+
}
|
|
39219
|
+
const db = createDb(env2.DB);
|
|
39220
|
+
const contentful = new ContentfulService(CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
39221
|
+
try {
|
|
39222
|
+
if (CONTENTFUL_MANAGEMENT_TOKEN) {
|
|
39223
|
+
const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
39224
|
+
await ensureContentTypes(mgmt);
|
|
39225
|
+
}
|
|
39226
|
+
c.executionCtx.waitUntil(
|
|
39227
|
+
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))
|
|
39228
|
+
);
|
|
39229
|
+
return c.json({ message: "Contentful sync triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
|
|
39230
|
+
} catch (err) {
|
|
39231
|
+
console.error("[Contentful] Sync trigger failed:", err);
|
|
39232
|
+
return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger sync" } }, 500);
|
|
39233
|
+
}
|
|
39234
|
+
}
|
|
39235
|
+
);
|
|
39236
|
+
contentfulSyncRoute.post(
|
|
39237
|
+
"/push",
|
|
39238
|
+
authMiddleware,
|
|
39239
|
+
adminMiddleware,
|
|
39240
|
+
async (c) => {
|
|
39241
|
+
const env2 = c.env;
|
|
39242
|
+
const { CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT } = env2;
|
|
39243
|
+
if (!CONTENTFUL_SPACE_ID || !CONTENTFUL_MANAGEMENT_TOKEN) {
|
|
39244
|
+
return c.json({ error: { code: "NOT_CONFIGURED", message: "Contentful management credentials missing" } }, 400);
|
|
39245
|
+
}
|
|
39246
|
+
const db = createDb(env2.DB);
|
|
39247
|
+
const mgmt = new ContentfulManagement(CONTENTFUL_SPACE_ID, CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_ENVIRONMENT);
|
|
39248
|
+
try {
|
|
39249
|
+
c.executionCtx.waitUntil(
|
|
39250
|
+
pushToContentful(db, mgmt, {}, env2.KV).then((r) => console.log("[Contentful] Push complete:", JSON.stringify(r))).catch((err) => console.error("[Contentful] Push failed:", err))
|
|
39251
|
+
);
|
|
39252
|
+
return c.json({ message: "Contentful push triggered", triggeredAt: Math.floor(Date.now() / 1e3) });
|
|
39253
|
+
} catch (err) {
|
|
39254
|
+
console.error("[Contentful] Push trigger failed:", err);
|
|
39255
|
+
return c.json({ error: { code: "SYNC_FAILED", message: err?.message ?? "Failed to trigger push" } }, 500);
|
|
39256
|
+
}
|
|
39257
|
+
}
|
|
39258
|
+
);
|
|
39259
|
+
contentfulSyncRoute.get(
|
|
39260
|
+
"/status",
|
|
39261
|
+
authMiddleware,
|
|
39262
|
+
adminMiddleware,
|
|
39263
|
+
async (c) => {
|
|
39264
|
+
const env2 = c.env;
|
|
39265
|
+
const db = createDb(env2.DB);
|
|
39266
|
+
const [[{ themes: themes2 }], [{ events: events2 }], [{ faqs: faqs2 }], [{ orgs }]] = await Promise.all([
|
|
39267
|
+
db.all(sql2`SELECT count(*) as themes FROM themes`),
|
|
39268
|
+
db.all(sql2`SELECT count(*) as events FROM events`),
|
|
39269
|
+
db.all(sql2`SELECT count(*) as faqs FROM faqs`),
|
|
39270
|
+
db.all(sql2`SELECT count(*) as orgs FROM organizations`)
|
|
39271
|
+
]);
|
|
39272
|
+
const isConfigured = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_ACCESS_TOKEN;
|
|
39273
|
+
const canPush = !!env2.CONTENTFUL_SPACE_ID && !!env2.CONTENTFUL_MANAGEMENT_TOKEN;
|
|
39274
|
+
return c.json({
|
|
39275
|
+
isConfigured,
|
|
39276
|
+
canPush,
|
|
39277
|
+
cmsMode: c.get("cmsMode"),
|
|
39278
|
+
spaceId: env2.CONTENTFUL_SPACE_ID ?? null,
|
|
39279
|
+
totals: { themes: themes2, events: events2, faqs: faqs2, organizations: orgs }
|
|
39280
|
+
});
|
|
39281
|
+
}
|
|
39282
|
+
);
|
|
39283
|
+
|
|
38681
39284
|
// src/app.ts
|
|
38682
39285
|
function createApp(options = {}) {
|
|
38683
39286
|
const app = new Hono();
|
|
@@ -38691,6 +39294,12 @@ function createApp(options = {}) {
|
|
|
38691
39294
|
app.use("*", createCorsMiddleware(options.allowedOrigins ?? ["*"]));
|
|
38692
39295
|
app.use("*", createPowChallengeMiddleware());
|
|
38693
39296
|
app.use("*", createRefererGuard(options.allowedOrigins ?? ["*"]));
|
|
39297
|
+
app.use("*", async (c, next) => {
|
|
39298
|
+
const overrideRaw = await c.env.KV.get("config:cms_mode").catch(() => null);
|
|
39299
|
+
const override = overrideRaw ? JSON.parse(overrideRaw) : null;
|
|
39300
|
+
c.set("cmsMode", parseCmsMode(override ?? c.env.CMS_MODE));
|
|
39301
|
+
return next();
|
|
39302
|
+
});
|
|
38694
39303
|
app.on(["POST", "GET"], "/api/auth/*", (c) => {
|
|
38695
39304
|
const auth = createAuth(c.env);
|
|
38696
39305
|
const req = c.req.raw;
|
|
@@ -38702,7 +39311,7 @@ function createApp(options = {}) {
|
|
|
38702
39311
|
return auth.handler(new Request(req.url, {
|
|
38703
39312
|
method: req.method,
|
|
38704
39313
|
headers: newHeaders,
|
|
38705
|
-
body: req.method === "GET" ? null : req.body,
|
|
39314
|
+
body: req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" ? null : req.body,
|
|
38706
39315
|
redirect: req.redirect
|
|
38707
39316
|
}));
|
|
38708
39317
|
}
|
|
@@ -38723,12 +39332,13 @@ function createApp(options = {}) {
|
|
|
38723
39332
|
app.post(POW_VERIFY_PATH, handlePowVerify);
|
|
38724
39333
|
app.route("/health", healthRoute);
|
|
38725
39334
|
app.route("/api/config", siteConfigRoute);
|
|
38726
|
-
app.route("/api/
|
|
39335
|
+
app.route("/api/classes", classesRoute);
|
|
38727
39336
|
app.route("/api/themes", themesRoute);
|
|
38728
39337
|
app.route("/api/users", usersRoute);
|
|
38729
39338
|
app.route("/api/organizations", organizationsRoute);
|
|
38730
39339
|
app.route("/api/faqs", faqsRoute);
|
|
38731
39340
|
app.route("/api/uploads", uploadsRoute);
|
|
39341
|
+
app.route("/api/contentful", contentfulSyncRoute);
|
|
38732
39342
|
app.route("/internal/gforms-webhook", gformsWebhookRoute);
|
|
38733
39343
|
app.onError(errorHandler);
|
|
38734
39344
|
app.notFound(
|
|
@@ -39362,6 +39972,7 @@ var PATCH_STATEMENTS = [
|
|
|
39362
39972
|
`ALTER TABLE "events" ADD COLUMN "class_code" text`,
|
|
39363
39973
|
`ALTER TABLE "events" ADD COLUMN "start_time" text`,
|
|
39364
39974
|
`ALTER TABLE "events" ADD COLUMN "end_time" text`,
|
|
39975
|
+
`ALTER TABLE "events" RENAME COLUMN "is_major" TO "is_spotlight"`,
|
|
39365
39976
|
`CREATE INDEX IF NOT EXISTS "idx_events_organization_id" ON "events" ("organization_id")`
|
|
39366
39977
|
];
|
|
39367
39978
|
var CREATE_STATEMENTS = [
|
|
@@ -39465,7 +40076,7 @@ var CREATE_STATEMENTS = [
|
|
|
39465
40076
|
"class_code" text,
|
|
39466
40077
|
"start_time" text,
|
|
39467
40078
|
"end_time" text,
|
|
39468
|
-
"
|
|
40079
|
+
"is_spotlight" integer DEFAULT false NOT NULL,
|
|
39469
40080
|
"max_slots" integer DEFAULT 0 NOT NULL,
|
|
39470
40081
|
"registered_slots" integer DEFAULT 0 NOT NULL,
|
|
39471
40082
|
"gforms_id" text,
|
|
@@ -39527,7 +40138,9 @@ async function ensureDatabase(d1) {
|
|
|
39527
40138
|
try {
|
|
39528
40139
|
await d1.prepare(sql3).run();
|
|
39529
40140
|
} catch (err) {
|
|
39530
|
-
if (err?.message?.includes("duplicate column"))
|
|
40141
|
+
if (err?.message?.includes("duplicate column") || err?.message?.includes("no such column") && err?.message?.includes("is_major")) {
|
|
40142
|
+
continue;
|
|
40143
|
+
}
|
|
39531
40144
|
throw err;
|
|
39532
40145
|
}
|
|
39533
40146
|
}
|
|
@@ -39536,12 +40149,14 @@ async function ensureDatabase(d1) {
|
|
|
39536
40149
|
// src/worker-handler.ts
|
|
39537
40150
|
var API_PREFIXES = [
|
|
39538
40151
|
"/api/auth/",
|
|
39539
|
-
"/api/
|
|
40152
|
+
"/api/classes",
|
|
39540
40153
|
"/api/users",
|
|
40154
|
+
"/api/organizations",
|
|
39541
40155
|
"/api/faqs",
|
|
39542
40156
|
"/api/themes",
|
|
39543
40157
|
"/api/config",
|
|
39544
40158
|
"/api/uploads",
|
|
40159
|
+
"/api/contentful",
|
|
39545
40160
|
"/health",
|
|
39546
40161
|
"/internal/",
|
|
39547
40162
|
"/.well-known/"
|
|
@@ -39580,7 +40195,7 @@ function createWorkerHandler(options) {
|
|
|
39580
40195
|
return getLeapify(env2).fetch(request, env2, ctx);
|
|
39581
40196
|
}
|
|
39582
40197
|
let response = await options.serveFrontend(request, env2, ctx);
|
|
39583
|
-
if ((!response || response.status === 404) && !pathname.includes(".")) {
|
|
40198
|
+
if ((!response || response.status === 404) && !pathname.includes(".") && request.method === "GET") {
|
|
39584
40199
|
const indexRequest = new Request(new URL("/", request.url), request);
|
|
39585
40200
|
response = await options.serveFrontend(indexRequest, env2, ctx);
|
|
39586
40201
|
}
|
|
@@ -39688,6 +40303,6 @@ function injectConfig2(html2, config3) {
|
|
|
39688
40303
|
(*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
|
|
39689
40304
|
*/
|
|
39690
40305
|
|
|
39691
|
-
export { ContentfulManagement, authAccount, authSession, authUser, authVerification, bookmarks, bookmarksRelations, createDb, createLeapify, createQueueHandler, createWorkerHandler, ensureContentTypes, ensureDatabase, events, eventsRelations, faqs, getRuntimeConfig, injectConfig2 as injectConfig, organizations, organizationsRelations, siteConfig, themes, themesRelations, users };
|
|
40306
|
+
export { CONTENTFUL_CONFIG_KEYS, ContentfulManagement, authAccount, authSession, authUser, authVerification, bookmarks, bookmarksRelations, contentfulConfig, createDb, createLeapify, createQueueHandler, createWorkerHandler, ensureContentTypes, ensureDatabase, events, eventsRelations, faqs, getRuntimeConfig, injectConfig2 as injectConfig, organizations, organizationsRelations, parseCmsMode, shouldPullFromContentful, shouldPushToContentful, siteConfig, themes, themesRelations, users };
|
|
39692
40307
|
//# sourceMappingURL=index.js.map
|
|
39693
40308
|
//# sourceMappingURL=index.js.map
|