@we8/cms 0.1.0

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.
Files changed (304) hide show
  1. package/README.md +78 -0
  2. package/dist/auth/auth.d.ts +87 -0
  3. package/dist/auth/auth.d.ts.map +1 -0
  4. package/dist/auth/auth.js +90 -0
  5. package/dist/auth/auth.js.map +1 -0
  6. package/dist/auth/routes.admin.d.ts +22 -0
  7. package/dist/auth/routes.admin.d.ts.map +1 -0
  8. package/dist/auth/routes.admin.js +106 -0
  9. package/dist/auth/routes.admin.js.map +1 -0
  10. package/dist/auth/schema.d.ts +1395 -0
  11. package/dist/auth/schema.d.ts.map +1 -0
  12. package/dist/auth/schema.js +76 -0
  13. package/dist/auth/schema.js.map +1 -0
  14. package/dist/env.d.ts +64 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +48 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +170 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/keys/middleware.d.ts +54 -0
  23. package/dist/keys/middleware.d.ts.map +1 -0
  24. package/dist/keys/middleware.js +84 -0
  25. package/dist/keys/middleware.js.map +1 -0
  26. package/dist/keys/routes.admin.d.ts +16 -0
  27. package/dist/keys/routes.admin.d.ts.map +1 -0
  28. package/dist/keys/routes.admin.js +34 -0
  29. package/dist/keys/routes.admin.js.map +1 -0
  30. package/dist/keys/service.d.ts +36 -0
  31. package/dist/keys/service.d.ts.map +1 -0
  32. package/dist/keys/service.js +120 -0
  33. package/dist/keys/service.js.map +1 -0
  34. package/dist/lib/bots.d.ts +24 -0
  35. package/dist/lib/bots.d.ts.map +1 -0
  36. package/dist/lib/bots.js +79 -0
  37. package/dist/lib/bots.js.map +1 -0
  38. package/dist/lib/email.d.ts +62 -0
  39. package/dist/lib/email.d.ts.map +1 -0
  40. package/dist/lib/email.js +64 -0
  41. package/dist/lib/email.js.map +1 -0
  42. package/dist/lib/errors.d.ts +12 -0
  43. package/dist/lib/errors.d.ts.map +1 -0
  44. package/dist/lib/errors.js +23 -0
  45. package/dist/lib/errors.js.map +1 -0
  46. package/dist/lib/notify.d.ts +20 -0
  47. package/dist/lib/notify.d.ts.map +1 -0
  48. package/dist/lib/notify.js +60 -0
  49. package/dist/lib/notify.js.map +1 -0
  50. package/dist/lib/retention.d.ts +15 -0
  51. package/dist/lib/retention.d.ts.map +1 -0
  52. package/dist/lib/retention.js +40 -0
  53. package/dist/lib/retention.js.map +1 -0
  54. package/dist/lib/sanitize.d.ts +14 -0
  55. package/dist/lib/sanitize.d.ts.map +1 -0
  56. package/dist/lib/sanitize.js +145 -0
  57. package/dist/lib/sanitize.js.map +1 -0
  58. package/dist/lib/turnstile.d.ts +7 -0
  59. package/dist/lib/turnstile.d.ts.map +1 -0
  60. package/dist/lib/turnstile.js +32 -0
  61. package/dist/lib/turnstile.js.map +1 -0
  62. package/dist/lib/validate.d.ts +11 -0
  63. package/dist/lib/validate.d.ts.map +1 -0
  64. package/dist/lib/validate.js +22 -0
  65. package/dist/lib/validate.js.map +1 -0
  66. package/dist/lib/word-count.d.ts +7 -0
  67. package/dist/lib/word-count.d.ts.map +1 -0
  68. package/dist/lib/word-count.js +14 -0
  69. package/dist/lib/word-count.js.map +1 -0
  70. package/dist/middleware/auth.d.ts +46 -0
  71. package/dist/middleware/auth.d.ts.map +1 -0
  72. package/dist/middleware/auth.js +60 -0
  73. package/dist/middleware/auth.js.map +1 -0
  74. package/dist/middleware/cors.d.ts +33 -0
  75. package/dist/middleware/cors.d.ts.map +1 -0
  76. package/dist/middleware/cors.js +55 -0
  77. package/dist/middleware/cors.js.map +1 -0
  78. package/dist/middleware/module-gate.d.ts +11 -0
  79. package/dist/middleware/module-gate.d.ts.map +1 -0
  80. package/dist/middleware/module-gate.js +18 -0
  81. package/dist/middleware/module-gate.js.map +1 -0
  82. package/dist/middleware/rate-limit.d.ts +13 -0
  83. package/dist/middleware/rate-limit.d.ts.map +1 -0
  84. package/dist/middleware/rate-limit.js +30 -0
  85. package/dist/middleware/rate-limit.js.map +1 -0
  86. package/dist/middleware/request-id.d.ts +8 -0
  87. package/dist/middleware/request-id.d.ts.map +1 -0
  88. package/dist/middleware/request-id.js +11 -0
  89. package/dist/middleware/request-id.js.map +1 -0
  90. package/dist/modules/aeo/checks.d.ts +41 -0
  91. package/dist/modules/aeo/checks.d.ts.map +1 -0
  92. package/dist/modules/aeo/checks.js +127 -0
  93. package/dist/modules/aeo/checks.js.map +1 -0
  94. package/dist/modules/aeo/llms.d.ts +24 -0
  95. package/dist/modules/aeo/llms.d.ts.map +1 -0
  96. package/dist/modules/aeo/llms.js +92 -0
  97. package/dist/modules/aeo/llms.js.map +1 -0
  98. package/dist/modules/aeo/routes.admin.d.ts +12 -0
  99. package/dist/modules/aeo/routes.admin.d.ts.map +1 -0
  100. package/dist/modules/aeo/routes.admin.js +37 -0
  101. package/dist/modules/aeo/routes.admin.js.map +1 -0
  102. package/dist/modules/analytics/db.d.ts +24 -0
  103. package/dist/modules/analytics/db.d.ts.map +1 -0
  104. package/dist/modules/analytics/db.js +24 -0
  105. package/dist/modules/analytics/db.js.map +1 -0
  106. package/dist/modules/analytics/routes.admin.d.ts +6 -0
  107. package/dist/modules/analytics/routes.admin.d.ts.map +1 -0
  108. package/dist/modules/analytics/routes.admin.js +21 -0
  109. package/dist/modules/analytics/routes.admin.js.map +1 -0
  110. package/dist/modules/analytics/routes.public.d.ts +6 -0
  111. package/dist/modules/analytics/routes.public.d.ts.map +1 -0
  112. package/dist/modules/analytics/routes.public.js +33 -0
  113. package/dist/modules/analytics/routes.public.js.map +1 -0
  114. package/dist/modules/analytics/service.d.ts +16 -0
  115. package/dist/modules/analytics/service.d.ts.map +1 -0
  116. package/dist/modules/analytics/service.js +148 -0
  117. package/dist/modules/analytics/service.js.map +1 -0
  118. package/dist/modules/authors/routes.admin.d.ts +6 -0
  119. package/dist/modules/authors/routes.admin.d.ts.map +1 -0
  120. package/dist/modules/authors/routes.admin.js +29 -0
  121. package/dist/modules/authors/routes.admin.js.map +1 -0
  122. package/dist/modules/authors/routes.public.d.ts +6 -0
  123. package/dist/modules/authors/routes.public.d.ts.map +1 -0
  124. package/dist/modules/authors/routes.public.js +11 -0
  125. package/dist/modules/authors/routes.public.js.map +1 -0
  126. package/dist/modules/authors/service.d.ts +26 -0
  127. package/dist/modules/authors/service.d.ts.map +1 -0
  128. package/dist/modules/authors/service.js +116 -0
  129. package/dist/modules/authors/service.js.map +1 -0
  130. package/dist/modules/content/db.d.ts +127 -0
  131. package/dist/modules/content/db.d.ts.map +1 -0
  132. package/dist/modules/content/db.js +203 -0
  133. package/dist/modules/content/db.js.map +1 -0
  134. package/dist/modules/content/publish.d.ts +38 -0
  135. package/dist/modules/content/publish.d.ts.map +1 -0
  136. package/dist/modules/content/publish.js +184 -0
  137. package/dist/modules/content/publish.js.map +1 -0
  138. package/dist/modules/content/routes.admin.d.ts +8 -0
  139. package/dist/modules/content/routes.admin.d.ts.map +1 -0
  140. package/dist/modules/content/routes.admin.js +122 -0
  141. package/dist/modules/content/routes.admin.js.map +1 -0
  142. package/dist/modules/content/routes.public.d.ts +6 -0
  143. package/dist/modules/content/routes.public.d.ts.map +1 -0
  144. package/dist/modules/content/routes.public.js +56 -0
  145. package/dist/modules/content/routes.public.js.map +1 -0
  146. package/dist/modules/content/service.d.ts +137 -0
  147. package/dist/modules/content/service.d.ts.map +1 -0
  148. package/dist/modules/content/service.js +348 -0
  149. package/dist/modules/content/service.js.map +1 -0
  150. package/dist/modules/forms/db.d.ts +42 -0
  151. package/dist/modules/forms/db.d.ts.map +1 -0
  152. package/dist/modules/forms/db.js +62 -0
  153. package/dist/modules/forms/db.js.map +1 -0
  154. package/dist/modules/forms/forms-routes.admin.d.ts +6 -0
  155. package/dist/modules/forms/forms-routes.admin.d.ts.map +1 -0
  156. package/dist/modules/forms/forms-routes.admin.js +37 -0
  157. package/dist/modules/forms/forms-routes.admin.js.map +1 -0
  158. package/dist/modules/forms/forms-service.d.ts +18 -0
  159. package/dist/modules/forms/forms-service.d.ts.map +1 -0
  160. package/dist/modules/forms/forms-service.js +121 -0
  161. package/dist/modules/forms/forms-service.js.map +1 -0
  162. package/dist/modules/forms/routes.public.d.ts +6 -0
  163. package/dist/modules/forms/routes.public.d.ts.map +1 -0
  164. package/dist/modules/forms/routes.public.js +41 -0
  165. package/dist/modules/forms/routes.public.js.map +1 -0
  166. package/dist/modules/forms/submissions-routes.admin.d.ts +6 -0
  167. package/dist/modules/forms/submissions-routes.admin.d.ts.map +1 -0
  168. package/dist/modules/forms/submissions-routes.admin.js +46 -0
  169. package/dist/modules/forms/submissions-routes.admin.js.map +1 -0
  170. package/dist/modules/forms/submissions-service.d.ts +45 -0
  171. package/dist/modules/forms/submissions-service.d.ts.map +1 -0
  172. package/dist/modules/forms/submissions-service.js +174 -0
  173. package/dist/modules/forms/submissions-service.js.map +1 -0
  174. package/dist/modules/media/db.d.ts +15 -0
  175. package/dist/modules/media/db.d.ts.map +1 -0
  176. package/dist/modules/media/db.js +24 -0
  177. package/dist/modules/media/db.js.map +1 -0
  178. package/dist/modules/media/routes.admin.d.ts +6 -0
  179. package/dist/modules/media/routes.admin.d.ts.map +1 -0
  180. package/dist/modules/media/routes.admin.js +30 -0
  181. package/dist/modules/media/routes.admin.js.map +1 -0
  182. package/dist/modules/media/routes.public.d.ts +6 -0
  183. package/dist/modules/media/routes.public.d.ts.map +1 -0
  184. package/dist/modules/media/routes.public.js +35 -0
  185. package/dist/modules/media/routes.public.js.map +1 -0
  186. package/dist/modules/media/service.d.ts +32 -0
  187. package/dist/modules/media/service.d.ts.map +1 -0
  188. package/dist/modules/media/service.js +99 -0
  189. package/dist/modules/media/service.js.map +1 -0
  190. package/dist/modules/openapi/spec.d.ts +21 -0
  191. package/dist/modules/openapi/spec.d.ts.map +1 -0
  192. package/dist/modules/openapi/spec.js +468 -0
  193. package/dist/modules/openapi/spec.js.map +1 -0
  194. package/dist/modules/seo/jsonld.d.ts +25 -0
  195. package/dist/modules/seo/jsonld.d.ts.map +1 -0
  196. package/dist/modules/seo/jsonld.js +67 -0
  197. package/dist/modules/seo/jsonld.js.map +1 -0
  198. package/dist/modules/seo/routes.public.d.ts +6 -0
  199. package/dist/modules/seo/routes.public.d.ts.map +1 -0
  200. package/dist/modules/seo/routes.public.js +39 -0
  201. package/dist/modules/seo/routes.public.js.map +1 -0
  202. package/dist/modules/seo/sitemap.d.ts +3 -0
  203. package/dist/modules/seo/sitemap.d.ts.map +1 -0
  204. package/dist/modules/seo/sitemap.js +39 -0
  205. package/dist/modules/seo/sitemap.js.map +1 -0
  206. package/dist/modules/settings/routes.admin.d.ts +12 -0
  207. package/dist/modules/settings/routes.admin.d.ts.map +1 -0
  208. package/dist/modules/settings/routes.admin.js +27 -0
  209. package/dist/modules/settings/routes.admin.js.map +1 -0
  210. package/dist/modules/site-documents/routes.admin.d.ts +10 -0
  211. package/dist/modules/site-documents/routes.admin.d.ts.map +1 -0
  212. package/dist/modules/site-documents/routes.admin.js +29 -0
  213. package/dist/modules/site-documents/routes.admin.js.map +1 -0
  214. package/dist/modules/site-documents/routes.public.d.ts +6 -0
  215. package/dist/modules/site-documents/routes.public.d.ts.map +1 -0
  216. package/dist/modules/site-documents/routes.public.js +16 -0
  217. package/dist/modules/site-documents/routes.public.js.map +1 -0
  218. package/dist/modules/site-documents/service.d.ts +11 -0
  219. package/dist/modules/site-documents/service.d.ts.map +1 -0
  220. package/dist/modules/site-documents/service.js +42 -0
  221. package/dist/modules/site-documents/service.js.map +1 -0
  222. package/dist/modules/webhooks/dispatch.d.ts +14 -0
  223. package/dist/modules/webhooks/dispatch.d.ts.map +1 -0
  224. package/dist/modules/webhooks/dispatch.js +84 -0
  225. package/dist/modules/webhooks/dispatch.js.map +1 -0
  226. package/dist/shared/constants.d.ts +79 -0
  227. package/dist/shared/constants.d.ts.map +1 -0
  228. package/dist/shared/constants.js +89 -0
  229. package/dist/shared/constants.js.map +1 -0
  230. package/dist/shared/envelope.d.ts +37 -0
  231. package/dist/shared/envelope.d.ts.map +1 -0
  232. package/dist/shared/envelope.js +46 -0
  233. package/dist/shared/envelope.js.map +1 -0
  234. package/dist/shared/hash.d.ts +2 -0
  235. package/dist/shared/hash.d.ts.map +1 -0
  236. package/dist/shared/hash.js +8 -0
  237. package/dist/shared/hash.js.map +1 -0
  238. package/dist/shared/index.d.ts +13 -0
  239. package/dist/shared/index.d.ts.map +1 -0
  240. package/dist/shared/index.js +13 -0
  241. package/dist/shared/index.js.map +1 -0
  242. package/dist/shared/schemas/aeo.d.ts +182 -0
  243. package/dist/shared/schemas/aeo.d.ts.map +1 -0
  244. package/dist/shared/schemas/aeo.js +43 -0
  245. package/dist/shared/schemas/aeo.js.map +1 -0
  246. package/dist/shared/schemas/analytics.d.ts +277 -0
  247. package/dist/shared/schemas/analytics.d.ts.map +1 -0
  248. package/dist/shared/schemas/analytics.js +74 -0
  249. package/dist/shared/schemas/analytics.js.map +1 -0
  250. package/dist/shared/schemas/api-key.d.ts +77 -0
  251. package/dist/shared/schemas/api-key.d.ts.map +1 -0
  252. package/dist/shared/schemas/api-key.js +27 -0
  253. package/dist/shared/schemas/api-key.js.map +1 -0
  254. package/dist/shared/schemas/author.d.ts +147 -0
  255. package/dist/shared/schemas/author.d.ts.map +1 -0
  256. package/dist/shared/schemas/author.js +27 -0
  257. package/dist/shared/schemas/author.js.map +1 -0
  258. package/dist/shared/schemas/form.d.ts +341 -0
  259. package/dist/shared/schemas/form.d.ts.map +1 -0
  260. package/dist/shared/schemas/form.js +117 -0
  261. package/dist/shared/schemas/form.js.map +1 -0
  262. package/dist/shared/schemas/index.d.ts +12 -0
  263. package/dist/shared/schemas/index.d.ts.map +1 -0
  264. package/dist/shared/schemas/index.js +12 -0
  265. package/dist/shared/schemas/index.js.map +1 -0
  266. package/dist/shared/schemas/media.d.ts +42 -0
  267. package/dist/shared/schemas/media.d.ts.map +1 -0
  268. package/dist/shared/schemas/media.js +16 -0
  269. package/dist/shared/schemas/media.js.map +1 -0
  270. package/dist/shared/schemas/pagination.d.ts +34 -0
  271. package/dist/shared/schemas/pagination.d.ts.map +1 -0
  272. package/dist/shared/schemas/pagination.js +21 -0
  273. package/dist/shared/schemas/pagination.js.map +1 -0
  274. package/dist/shared/schemas/post.d.ts +862 -0
  275. package/dist/shared/schemas/post.d.ts.map +1 -0
  276. package/dist/shared/schemas/post.js +187 -0
  277. package/dist/shared/schemas/post.js.map +1 -0
  278. package/dist/shared/schemas/settings.d.ts +839 -0
  279. package/dist/shared/schemas/settings.d.ts.map +1 -0
  280. package/dist/shared/schemas/settings.js +202 -0
  281. package/dist/shared/schemas/settings.js.map +1 -0
  282. package/dist/shared/schemas/site-document.d.ts +38 -0
  283. package/dist/shared/schemas/site-document.d.ts.map +1 -0
  284. package/dist/shared/schemas/site-document.js +21 -0
  285. package/dist/shared/schemas/site-document.js.map +1 -0
  286. package/dist/shared/schemas/webhook.d.ts +39 -0
  287. package/dist/shared/schemas/webhook.d.ts.map +1 -0
  288. package/dist/shared/schemas/webhook.js +20 -0
  289. package/dist/shared/schemas/webhook.js.map +1 -0
  290. package/dist/spa.d.ts +9 -0
  291. package/dist/spa.d.ts.map +1 -0
  292. package/dist/spa.js +57 -0
  293. package/dist/spa.js.map +1 -0
  294. package/migrations/0001_settings.sql +39 -0
  295. package/migrations/0002_content.sql +135 -0
  296. package/migrations/0003_forms.sql +35 -0
  297. package/migrations/0004_analytics.sql +42 -0
  298. package/migrations/0005_media.sql +17 -0
  299. package/migrations/0006_site_documents.sql +18 -0
  300. package/migrations/0007_auth.sql +81 -0
  301. package/migrations/0008_api_keys.sql +20 -0
  302. package/package.json +68 -0
  303. package/scripts/bundle-admin.mjs +54 -0
  304. package/seed/local.sql +63 -0
@@ -0,0 +1,56 @@
1
+ import { ok, resolveContentPath, postListQuerySchema } from '../../shared/index.js';
2
+ import { Hono } from 'hono';
3
+ import { z } from 'zod';
4
+ import { getSettings } from '../../env.js';
5
+ import { parseOrThrow } from '../../lib/validate.js';
6
+ import { corsMiddleware, enforceOrigin } from '../../middleware/cors.js';
7
+ import { rateLimit } from '../../middleware/rate-limit.js';
8
+ import { getAuthorsByIds } from '../authors/service.js';
9
+ import { buildPostJsonLd } from '../seo/jsonld.js';
10
+ import { getPublishedPostBySlug, listPublishedPosts, recordView, toggleLike } from './service.js';
11
+ const sessionIdBodySchema = z.object({ sessionId: z.string().min(1).max(200) });
12
+ async function parseJsonBody(request) {
13
+ try {
14
+ return await request.json();
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ export const contentPublicRoutes = new Hono();
21
+ contentPublicRoutes.use('/', corsMiddleware('publicRead'));
22
+ contentPublicRoutes.use('/:slug', corsMiddleware('publicRead'));
23
+ contentPublicRoutes.use('/:slug/view', corsMiddleware('publicWrite'), enforceOrigin, rateLimit('engagement'));
24
+ contentPublicRoutes.use('/:slug/like', corsMiddleware('publicWrite'), enforceOrigin, rateLimit('engagement'));
25
+ contentPublicRoutes.get('/', async (c) => {
26
+ const query = parseOrThrow(postListQuerySchema, c.req.query());
27
+ const { items, total } = await listPublishedPosts(c.env.DB, query);
28
+ return c.json(ok({ items, page: query.page, pageSize: query.pageSize, total }));
29
+ });
30
+ contentPublicRoutes.get('/:slug', async (c) => {
31
+ const post = await getPublishedPostBySlug(c.env.DB, c.req.param('slug'));
32
+ const [settings, authors] = await Promise.all([
33
+ getSettings(c.env.DB),
34
+ getAuthorsByIds(c.env.DB, post.authorIds),
35
+ ]);
36
+ const jsonld = buildPostJsonLd(post, authors, c.env.SITE_URL, resolveContentPath(post.type, settings), settings.publisher);
37
+ // `authors` ships resolved (not just post.authorIds): a public consumer has
38
+ // no way to turn ids into names/avatars (the authors public route is
39
+ // by-slug only), yet any frontend rendering a byline needs them, and the
40
+ // route already loads them for the JSON-LD above. Found migrating the
41
+ // frontend migration, where the legacy API returned authors inline.
42
+ return c.json(ok({ post, authors, jsonld }));
43
+ });
44
+ contentPublicRoutes.post('/:slug/view', async (c) => {
45
+ const body = parseOrThrow(sessionIdBodySchema, await parseJsonBody(c.req.raw));
46
+ const post = await getPublishedPostBySlug(c.env.DB, c.req.param('slug'));
47
+ await recordView(c.env.DB, post.postId, body.sessionId);
48
+ return c.json(ok({}));
49
+ });
50
+ contentPublicRoutes.post('/:slug/like', async (c) => {
51
+ const body = parseOrThrow(sessionIdBodySchema, await parseJsonBody(c.req.raw));
52
+ const post = await getPublishedPostBySlug(c.env.DB, c.req.param('slug'));
53
+ const result = await toggleLike(c.env.DB, post.postId, body.sessionId);
54
+ return c.json(ok(result));
55
+ });
56
+ //# sourceMappingURL=routes.public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.public.js","sourceRoot":"","sources":["../../../src/modules/content/routes.public.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,137 @@
1
+ import type { ContentStats, CreatePostInput, PostListQuery, PostRecord, UpdatePostInput } from '../../shared/index.js';
2
+ export declare function createPost(db: D1Database, input: CreatePostInput): Promise<PostRecord>;
3
+ /**
4
+ * Routes an edit to its correct home: directly onto `posts` if the post has
5
+ * never been published, or forked into `post_drafts` (upserted) if it has.
6
+ * Editing a published post upserts into post_drafts only; the posts row is
7
+ * untouched until publish.
8
+ */
9
+ export declare function updateWorkingCopy(db: D1Database, postId: string, patch: UpdatePostInput): Promise<PostRecord>;
10
+ /**
11
+ * The admin-facing "what should the editor show" resolution: `posts` alone
12
+ * is not enough once a pending draft exists: because an edit to a published
13
+ * post lands in post_drafts and leaves the posts row alone, that row's
14
+ * content, title, and the rest stay the last-PUBLISHED values while a draft
15
+ * is pending, not the being-edited ones. Non-content fields (status,
16
+ * currentVersionId, viewCount, timestamps, ...) always come from `posts`
17
+ * regardless, since post_drafts carries no equivalents for those.
18
+ */
19
+ export declare function resolveAdminPostRecord(db: D1Database, postId: string): Promise<PostRecord>;
20
+ export declare function getPostAdmin(db: D1Database, postId: string): Promise<PostRecord>;
21
+ export interface ListPostsAdminQuery {
22
+ page: number;
23
+ pageSize: number;
24
+ status?: string | undefined;
25
+ type?: string | undefined;
26
+ category?: string | undefined;
27
+ }
28
+ export declare function listPostsAdmin(db: D1Database, query: ListPostsAdminQuery): Promise<{
29
+ items: PostRecord[];
30
+ total: number;
31
+ }>;
32
+ /**
33
+ * Content performance, all-time rather than day-windowed:
34
+ * view_count/like_count are already-cumulative derived columns (see
35
+ * recordView/toggleLike below), so there is no per-day breakdown to slice
36
+ * without joining post_views/post_likes by date, which nothing here needs.
37
+ * Ranked by view_count regardless of current status (an archived post that
38
+ * was once popular still deserves to show up; the Status column, not a
39
+ * filter, communicates that it is no longer live).
40
+ */
41
+ export declare function getContentStats(db: D1Database, limit?: number): Promise<ContentStats>;
42
+ export declare function hardDeletePost(db: D1Database, postId: string): Promise<void>;
43
+ /**
44
+ * Clones the current working copy into a brand new, never-published draft.
45
+ * Publish history is deliberately not copied: the duplicate is a new piece of
46
+ * content that happens to start from this one's text, not a fork of its past.
47
+ */
48
+ export declare function duplicatePost(db: D1Database, postId: string): Promise<PostRecord>;
49
+ /**
50
+ * Counters are DERIVED from the tracking tables inside the same batch, never
51
+ * incremented/decremented blind. A blind `like_count - 1` drifts under
52
+ * concurrent requests (two unlikes for the same session both pass the
53
+ * existence check, the second DELETE removes zero rows, both decrements run,
54
+ * and the counter can go negative). COUNT(*) in the same atomic batch is
55
+ * idempotent and cannot drift.
56
+ */
57
+ export declare function recordView(db: D1Database, postId: string, sessionId: string): Promise<void>;
58
+ export interface ToggleLikeResult {
59
+ liked: boolean;
60
+ likeCount: number;
61
+ }
62
+ export declare function toggleLike(db: D1Database, postId: string, sessionId: string): Promise<ToggleLikeResult>;
63
+ /**
64
+ * Public reads serve from the current version snapshot, not the posts mirror. `pv.*` already carries `post_id` (the FK back
65
+ * to posts); deliberately not also aliasing `posts.id`, which would collide
66
+ * with that same column name in the result row.
67
+ */
68
+ export declare function getPublishedPostBySlug(db: D1Database, slug: string): Promise<{
69
+ slug: string;
70
+ type: "blog" | "article" | "news" | "research" | "whitepaper" | "case-study";
71
+ viewCount: number;
72
+ likeCount: number;
73
+ id: string;
74
+ title: string;
75
+ excerpt: string | null;
76
+ content: string;
77
+ coverImageUrl: string | null;
78
+ category: string | null;
79
+ tags: string[];
80
+ ogTitle: string | null;
81
+ ogDescription: string | null;
82
+ ogImageUrl: string | null;
83
+ answerSummary: string | null;
84
+ questionHeading: string | null;
85
+ authorIds: string[];
86
+ cta: {
87
+ style: "link" | "gated-download";
88
+ buttonLabel: string;
89
+ url?: string | undefined;
90
+ heading?: string | undefined;
91
+ formKey?: string | undefined;
92
+ assetUrl?: string | undefined;
93
+ } | null;
94
+ note: string | null;
95
+ version: number;
96
+ wordCount: number | null;
97
+ publishedAt: string;
98
+ postId: string;
99
+ publishedBy: string | null;
100
+ }>;
101
+ export declare function listPublishedPosts(db: D1Database, query: PostListQuery): Promise<{
102
+ items: {
103
+ slug: string;
104
+ type: "blog" | "article" | "news" | "research" | "whitepaper" | "case-study";
105
+ viewCount: number;
106
+ likeCount: number;
107
+ id: string;
108
+ title: string;
109
+ excerpt: string | null;
110
+ content: string;
111
+ coverImageUrl: string | null;
112
+ category: string | null;
113
+ tags: string[];
114
+ ogTitle: string | null;
115
+ ogDescription: string | null;
116
+ ogImageUrl: string | null;
117
+ answerSummary: string | null;
118
+ questionHeading: string | null;
119
+ authorIds: string[];
120
+ cta: {
121
+ style: "link" | "gated-download";
122
+ buttonLabel: string;
123
+ url?: string | undefined;
124
+ heading?: string | undefined;
125
+ formKey?: string | undefined;
126
+ assetUrl?: string | undefined;
127
+ } | null;
128
+ note: string | null;
129
+ version: number;
130
+ wordCount: number | null;
131
+ publishedAt: string;
132
+ postId: string;
133
+ publishedBy: string | null;
134
+ }[];
135
+ total: number;
136
+ }>;
137
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/modules/content/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EAEf,aAAa,EACb,UAAU,EAGV,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAsB/B,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CA0C5F;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,UAAU,CAAC,CAuCrB;AAoGD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAchG;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAEtF;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCjD;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,YAAY,CAAC,CA6CvF;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAsBvF;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBjG;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,gBAAgB,CAAC,CA6B3B;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqBxE;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkD5E"}
@@ -0,0 +1,348 @@
1
+ import { HttpError } from '../../lib/errors.js';
2
+ import { sanitizeHtml } from '../../lib/sanitize.js';
3
+ import { countWords } from '../../lib/word-count.js';
4
+ import { assertAuthorsExist, currentAuthorIds, draftRowToContentFields, fetchDraftRow, fetchPostRow, fetchPostRowBySlug, generateUniqueSlug, hasDraft, mergeContentFields, postRowToContentFields, syncPostAuthorsStatements, toPostRecord, toPostVersionRecord, } from './db.js';
5
+ export async function createPost(db, input) {
6
+ const existing = await fetchPostRowBySlug(db, input.slug);
7
+ if (existing) {
8
+ throw new HttpError('validation_error', `slug "${input.slug}" is already in use`);
9
+ }
10
+ await assertAuthorsExist(db, input.authorIds);
11
+ const sanitizedContent = await sanitizeHtml(input.content);
12
+ const wordCount = countWords(sanitizedContent);
13
+ const id = crypto.randomUUID();
14
+ await db.batch([
15
+ db
16
+ .prepare(`INSERT INTO posts
17
+ (id, slug, type, title, excerpt, content, cover_image_url, category, tags, status, og_title, og_description, og_image_url, answer_summary, question_heading, cta, word_count)
18
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 'draft', ?10, ?11, ?12, ?13, ?14, ?15, ?16)`)
19
+ .bind(id, input.slug, input.type, input.title,
20
+ // `|| null` not `?? null`: "" means cleared/absent on the wire (see
21
+ // mergeContentFields) and must never be stored as an empty string.
22
+ input.excerpt || null, sanitizedContent, input.coverImageUrl || null, input.category || null, JSON.stringify(input.tags), input.ogTitle || null, input.ogDescription || null, input.ogImageUrl || null, input.answerSummary || null, input.questionHeading || null, input.cta ? JSON.stringify(input.cta) : null, wordCount),
23
+ ...syncPostAuthorsStatements(db, id, input.authorIds),
24
+ ]);
25
+ return toPostRecord(await fetchPostRow(db, id), false);
26
+ }
27
+ /**
28
+ * Routes an edit to its correct home: directly onto `posts` if the post has
29
+ * never been published, or forked into `post_drafts` (upserted) if it has.
30
+ * Editing a published post upserts into post_drafts only; the posts row is
31
+ * untouched until publish.
32
+ */
33
+ export async function updateWorkingCopy(db, postId, patch) {
34
+ const row = await fetchPostRow(db, postId);
35
+ // Slugs are immutable once published (post_drafts/post_versions carry no
36
+ // slug column; permalinks must not change under a live URL). Reject
37
+ // explicitly rather than silently dropping the field: a caller who sent a
38
+ // new slug and got a 200 back would reasonably believe it took.
39
+ if (row.current_version_id !== null && patch.slug !== undefined && patch.slug !== row.slug) {
40
+ throw new HttpError('validation_error', 'slug cannot be changed after a post has been published');
41
+ }
42
+ // Type is locked after publish for the same reason as slug: it selects the
43
+ // site section (contentPaths), so changing it moves a live URL.
44
+ if (row.current_version_id !== null && patch.type !== undefined && patch.type !== row.type) {
45
+ throw new HttpError('validation_error', 'type cannot be changed after a post has been published');
46
+ }
47
+ if (patch.authorIds) {
48
+ await assertAuthorsExist(db, patch.authorIds);
49
+ }
50
+ let sanitizedContent;
51
+ if (patch.content !== undefined) {
52
+ sanitizedContent = await sanitizeHtml(patch.content);
53
+ }
54
+ if (row.current_version_id === null) {
55
+ await updateNeverPublished(db, row, patch, sanitizedContent);
56
+ }
57
+ else {
58
+ await upsertDraft(db, postId, patch, sanitizedContent);
59
+ }
60
+ return resolveAdminPostRecord(db, postId);
61
+ }
62
+ async function updateNeverPublished(db, row, patch, sanitizedContent) {
63
+ if (patch.slug && patch.slug !== row.slug) {
64
+ const existing = await fetchPostRowBySlug(db, patch.slug);
65
+ if (existing) {
66
+ throw new HttpError('validation_error', `slug "${patch.slug}" is already in use`);
67
+ }
68
+ }
69
+ const merged = mergeContentFields(postRowToContentFields(row), patch, sanitizedContent);
70
+ const wordCount = sanitizedContent !== undefined ? countWords(sanitizedContent) : row.word_count;
71
+ const statements = [
72
+ db
73
+ .prepare(`UPDATE posts SET
74
+ slug=?2, type=?3, title=?4, excerpt=?5, content=?6, cover_image_url=?7, category=?8, tags=?9,
75
+ og_title=?10, og_description=?11, og_image_url=?12, answer_summary=?13, question_heading=?14,
76
+ cta=?15, word_count=?16, updated_at=datetime('now')
77
+ WHERE id=?1`)
78
+ .bind(row.id, patch.slug ?? row.slug, patch.type ?? row.type, merged.title, merged.excerpt, merged.content, merged.coverImageUrl, merged.category, JSON.stringify(merged.tags), merged.ogTitle, merged.ogDescription, merged.ogImageUrl, merged.answerSummary, merged.questionHeading, merged.cta ? JSON.stringify(merged.cta) : null, wordCount),
79
+ ];
80
+ if (patch.authorIds) {
81
+ statements.push(...syncPostAuthorsStatements(db, row.id, patch.authorIds));
82
+ }
83
+ await db.batch(statements);
84
+ }
85
+ async function upsertDraft(db, postId, patch, sanitizedContent) {
86
+ const existingDraft = await fetchDraftRow(db, postId);
87
+ const base = existingDraft
88
+ ? draftRowToContentFields(existingDraft)
89
+ : postRowToContentFields(await fetchPostRow(db, postId));
90
+ const baseAuthorIds = existingDraft
91
+ ? JSON.parse(existingDraft.author_ids)
92
+ : await currentAuthorIds(db, postId);
93
+ const merged = mergeContentFields(base, patch, sanitizedContent);
94
+ const authorIds = patch.authorIds ?? baseAuthorIds;
95
+ await db
96
+ .prepare(`INSERT INTO post_drafts
97
+ (post_id, title, excerpt, content, cover_image_url, category, tags, og_title, og_description, og_image_url, answer_summary, question_heading, cta, author_ids, updated_at)
98
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, datetime('now'))
99
+ ON CONFLICT(post_id) DO UPDATE SET
100
+ title=excluded.title, excerpt=excluded.excerpt, content=excluded.content,
101
+ cover_image_url=excluded.cover_image_url, category=excluded.category, tags=excluded.tags,
102
+ og_title=excluded.og_title, og_description=excluded.og_description, og_image_url=excluded.og_image_url,
103
+ answer_summary=excluded.answer_summary, question_heading=excluded.question_heading,
104
+ cta=excluded.cta, author_ids=excluded.author_ids, updated_at=datetime('now')`)
105
+ .bind(postId, merged.title, merged.excerpt, merged.content, merged.coverImageUrl, merged.category, JSON.stringify(merged.tags), merged.ogTitle, merged.ogDescription, merged.ogImageUrl, merged.answerSummary, merged.questionHeading, merged.cta ? JSON.stringify(merged.cta) : null, JSON.stringify(authorIds))
106
+ .run();
107
+ }
108
+ /**
109
+ * The admin-facing "what should the editor show" resolution: `posts` alone
110
+ * is not enough once a pending draft exists: because an edit to a published
111
+ * post lands in post_drafts and leaves the posts row alone, that row's
112
+ * content, title, and the rest stay the last-PUBLISHED values while a draft
113
+ * is pending, not the being-edited ones. Non-content fields (status,
114
+ * currentVersionId, viewCount, timestamps, ...) always come from `posts`
115
+ * regardless, since post_drafts carries no equivalents for those.
116
+ */
117
+ export async function resolveAdminPostRecord(db, postId) {
118
+ const row = await fetchPostRow(db, postId);
119
+ const draft = await fetchDraftRow(db, postId);
120
+ if (!draft) {
121
+ return toPostRecord(row, false, await currentAuthorIds(db, postId));
122
+ }
123
+ const contentFields = draftRowToContentFields(draft);
124
+ return {
125
+ ...toPostRecord(row, true, JSON.parse(draft.author_ids)),
126
+ ...contentFields,
127
+ wordCount: countWords(contentFields.content),
128
+ };
129
+ }
130
+ export async function getPostAdmin(db, postId) {
131
+ return resolveAdminPostRecord(db, postId);
132
+ }
133
+ export async function listPostsAdmin(db, query) {
134
+ const offset = (query.page - 1) * query.pageSize;
135
+ const conditions = [];
136
+ const bindings = [];
137
+ if (query.status) {
138
+ conditions.push(`status = ?${bindings.length + 1}`);
139
+ bindings.push(query.status);
140
+ }
141
+ if (query.type) {
142
+ conditions.push(`type = ?${bindings.length + 1}`);
143
+ bindings.push(query.type);
144
+ }
145
+ if (query.category) {
146
+ conditions.push(`category = ?${bindings.length + 1}`);
147
+ bindings.push(query.category);
148
+ }
149
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
150
+ const { results } = await db
151
+ .prepare(`SELECT * FROM posts ${whereClause} ORDER BY created_at DESC LIMIT ?${bindings.length + 1} OFFSET ?${bindings.length + 2}`)
152
+ .bind(...bindings, query.pageSize, offset)
153
+ .all();
154
+ const { results: countResults } = await db
155
+ .prepare(`SELECT COUNT(*) as count FROM posts ${whereClause}`)
156
+ .bind(...bindings)
157
+ .all();
158
+ const items = await Promise.all(results.map(async (row) => toPostRecord(row, await hasDraft(db, row.id))));
159
+ return { items, total: countResults[0]?.count ?? 0 };
160
+ }
161
+ /**
162
+ * Content performance, all-time rather than day-windowed:
163
+ * view_count/like_count are already-cumulative derived columns (see
164
+ * recordView/toggleLike below), so there is no per-day breakdown to slice
165
+ * without joining post_views/post_likes by date, which nothing here needs.
166
+ * Ranked by view_count regardless of current status (an archived post that
167
+ * was once popular still deserves to show up; the Status column, not a
168
+ * filter, communicates that it is no longer live).
169
+ */
170
+ export async function getContentStats(db, limit = 10) {
171
+ const totals = await db
172
+ .prepare(`SELECT
173
+ COALESCE(SUM(view_count), 0) as totalViews,
174
+ COALESCE(SUM(like_count), 0) as totalLikes,
175
+ SUM(CASE WHEN status = 'published' THEN 1 ELSE 0 END) as publishedCount
176
+ FROM posts`)
177
+ .first();
178
+ const { results } = await db
179
+ .prepare(`SELECT id, slug, title, type, status, view_count, like_count, published_at
180
+ FROM posts
181
+ ORDER BY view_count DESC, like_count DESC, created_at DESC
182
+ LIMIT ?1`)
183
+ .bind(limit)
184
+ .all();
185
+ return {
186
+ totalViews: totals?.totalViews ?? 0,
187
+ totalLikes: totals?.totalLikes ?? 0,
188
+ publishedCount: totals?.publishedCount ?? 0,
189
+ topPosts: results.map((row) => ({
190
+ id: row.id,
191
+ slug: row.slug,
192
+ title: row.title,
193
+ type: row.type,
194
+ status: row.status,
195
+ viewCount: row.view_count,
196
+ likeCount: row.like_count,
197
+ publishedAt: row.published_at,
198
+ })),
199
+ };
200
+ }
201
+ export async function hardDeletePost(db, postId) {
202
+ await fetchPostRow(db, postId); // 404s if missing
203
+ await db.batch([
204
+ db.prepare('DELETE FROM post_drafts WHERE post_id = ?1').bind(postId),
205
+ db.prepare('DELETE FROM post_versions WHERE post_id = ?1').bind(postId),
206
+ db.prepare('DELETE FROM post_authors WHERE post_id = ?1').bind(postId),
207
+ db.prepare('DELETE FROM post_views WHERE post_id = ?1').bind(postId),
208
+ db.prepare('DELETE FROM post_likes WHERE post_id = ?1').bind(postId),
209
+ db.prepare('DELETE FROM posts WHERE id = ?1').bind(postId),
210
+ ]);
211
+ }
212
+ /**
213
+ * Clones the current working copy into a brand new, never-published draft.
214
+ * Publish history is deliberately not copied: the duplicate is a new piece of
215
+ * content that happens to start from this one's text, not a fork of its past.
216
+ */
217
+ export async function duplicatePost(db, postId) {
218
+ const row = await fetchPostRow(db, postId);
219
+ const authorIds = await currentAuthorIds(db, postId);
220
+ const newSlug = await generateUniqueSlug(db, `${row.slug}-copy`);
221
+ return createPost(db, {
222
+ slug: newSlug,
223
+ type: row.type,
224
+ title: `${row.title} (copy)`,
225
+ excerpt: row.excerpt ?? undefined,
226
+ content: row.content, // already sanitized; createPost re-sanitizes, which is idempotent
227
+ coverImageUrl: row.cover_image_url ?? undefined,
228
+ category: row.category ?? undefined,
229
+ tags: JSON.parse(row.tags),
230
+ ogTitle: row.og_title ?? undefined,
231
+ ogDescription: row.og_description ?? undefined,
232
+ ogImageUrl: row.og_image_url ?? undefined,
233
+ answerSummary: row.answer_summary ?? undefined,
234
+ questionHeading: row.question_heading ?? undefined,
235
+ cta: row.cta ? JSON.parse(row.cta) : undefined,
236
+ authorIds,
237
+ });
238
+ }
239
+ /**
240
+ * Counters are DERIVED from the tracking tables inside the same batch, never
241
+ * incremented/decremented blind. A blind `like_count - 1` drifts under
242
+ * concurrent requests (two unlikes for the same session both pass the
243
+ * existence check, the second DELETE removes zero rows, both decrements run,
244
+ * and the counter can go negative). COUNT(*) in the same atomic batch is
245
+ * idempotent and cannot drift.
246
+ */
247
+ export async function recordView(db, postId, sessionId) {
248
+ await db.batch([
249
+ // The scalar subquery captures whatever version is current AT INSERT
250
+ // TIME, a permanent snapshot: post_versions rows are
251
+ // immutable and insert-only, so this stamp never needs revisiting once
252
+ // written. INSERT OR IGNORE means a session's first view of a post wins
253
+ // the stamp; a later re-view under a newer version does not overwrite it.
254
+ db
255
+ .prepare(`INSERT OR IGNORE INTO post_views (post_id, session_id, version_number)
256
+ VALUES (?1, ?2, (SELECT current_version_number FROM posts WHERE id = ?1))`)
257
+ .bind(postId, sessionId),
258
+ db
259
+ .prepare('UPDATE posts SET view_count = (SELECT COUNT(*) FROM post_views WHERE post_id = ?1) WHERE id = ?1')
260
+ .bind(postId),
261
+ ]);
262
+ }
263
+ export async function toggleLike(db, postId, sessionId) {
264
+ const existing = await db
265
+ .prepare('SELECT 1 FROM post_likes WHERE post_id = ?1 AND session_id = ?2')
266
+ .bind(postId, sessionId)
267
+ .first();
268
+ const mutation = existing
269
+ ? db
270
+ .prepare('DELETE FROM post_likes WHERE post_id = ?1 AND session_id = ?2')
271
+ .bind(postId, sessionId)
272
+ : db
273
+ // Same at-insert-time version stamp as recordView; see its comment.
274
+ .prepare(`INSERT OR IGNORE INTO post_likes (post_id, session_id, version_number)
275
+ VALUES (?1, ?2, (SELECT current_version_number FROM posts WHERE id = ?1))`)
276
+ .bind(postId, sessionId);
277
+ await db.batch([
278
+ mutation,
279
+ db
280
+ .prepare('UPDATE posts SET like_count = (SELECT COUNT(*) FROM post_likes WHERE post_id = ?1) WHERE id = ?1')
281
+ .bind(postId),
282
+ ]);
283
+ const row = await fetchPostRow(db, postId);
284
+ return { liked: !existing, likeCount: row.like_count };
285
+ }
286
+ /**
287
+ * Public reads serve from the current version snapshot, not the posts mirror. `pv.*` already carries `post_id` (the FK back
288
+ * to posts); deliberately not also aliasing `posts.id`, which would collide
289
+ * with that same column name in the result row.
290
+ */
291
+ export async function getPublishedPostBySlug(db, slug) {
292
+ const row = await db
293
+ .prepare(`SELECT posts.slug, posts.type, posts.view_count, posts.like_count, pv.*
294
+ FROM posts JOIN post_versions pv ON pv.id = posts.current_version_id
295
+ WHERE posts.slug = ?1 AND posts.status = 'published'`)
296
+ .bind(slug)
297
+ .first();
298
+ if (!row)
299
+ throw new HttpError('not_found', 'post not found');
300
+ return {
301
+ ...toPostVersionRecord(row),
302
+ slug: row.slug,
303
+ type: row.type,
304
+ viewCount: row.view_count,
305
+ likeCount: row.like_count,
306
+ };
307
+ }
308
+ export async function listPublishedPosts(db, query) {
309
+ const offset = (query.page - 1) * query.pageSize;
310
+ const conditions = ["posts.status = 'published'"];
311
+ const bindings = [];
312
+ if (query.category) {
313
+ conditions.push(`pv.category = ?${bindings.length + 1}`);
314
+ bindings.push(query.category);
315
+ }
316
+ if (query.tag) {
317
+ conditions.push(`pv.tags LIKE ?${bindings.length + 1}`);
318
+ bindings.push(`%"${query.tag}"%`);
319
+ }
320
+ if (query.type) {
321
+ conditions.push(`posts.type = ?${bindings.length + 1}`);
322
+ bindings.push(query.type);
323
+ }
324
+ const where = conditions.join(' AND ');
325
+ const { results } = await db
326
+ .prepare(`SELECT posts.slug, posts.type, posts.view_count, posts.like_count, pv.*
327
+ FROM posts JOIN post_versions pv ON pv.id = posts.current_version_id
328
+ WHERE ${where}
329
+ ORDER BY pv.published_at DESC
330
+ LIMIT ?${bindings.length + 1} OFFSET ?${bindings.length + 2}`)
331
+ .bind(...bindings, query.pageSize, offset)
332
+ .all();
333
+ const { results: countResults } = await db
334
+ .prepare(`SELECT COUNT(*) as count FROM posts JOIN post_versions pv ON pv.id = posts.current_version_id WHERE ${where}`)
335
+ .bind(...bindings)
336
+ .all();
337
+ return {
338
+ items: results.map((row) => ({
339
+ ...toPostVersionRecord(row),
340
+ slug: row.slug,
341
+ type: row.type,
342
+ viewCount: row.view_count,
343
+ likeCount: row.like_count,
344
+ })),
345
+ total: countResults[0]?.count ?? 0,
346
+ };
347
+ }
348
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../../src/modules/content/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { FormRecord, FormSettings, FormType, SubmissionRecord, SubmissionStatus } from '../../shared/index.js';
2
+ export interface FormRow {
3
+ id: string;
4
+ form_key: string;
5
+ name: string;
6
+ type: FormType;
7
+ settings: string;
8
+ created_at: string;
9
+ updated_at: string | null;
10
+ archived_at: string | null;
11
+ }
12
+ /** A form row plus the submission aggregates the list/get queries LEFT JOIN in. */
13
+ export interface FormListRow extends FormRow {
14
+ submission_count: number;
15
+ last_submission_at: string | null;
16
+ }
17
+ export interface SubmissionRow {
18
+ id: string;
19
+ form_id: string;
20
+ submitted_at: string;
21
+ payload: string;
22
+ email: string | null;
23
+ name: string | null;
24
+ status: SubmissionStatus;
25
+ notes: string | null;
26
+ ip_hash: string | null;
27
+ user_agent: string | null;
28
+ source_page: string | null;
29
+ referrer: string | null;
30
+ }
31
+ /** A submission row joined with its form's name, for the inbox display. */
32
+ export interface SubmissionRowWithForm extends SubmissionRow {
33
+ form_name: string | null;
34
+ }
35
+ export declare function parseFormSettings(settingsJson: string): FormSettings;
36
+ export declare function toFormRecord(row: FormListRow): FormRecord;
37
+ export declare function toSubmissionRecord(row: SubmissionRowWithForm): SubmissionRecord;
38
+ export declare function fetchFormByKey(db: D1Database, formKey: string): Promise<FormRow>;
39
+ export declare function fetchFormById(db: D1Database, id: string): Promise<FormRow>;
40
+ /** Single submission joined with its form name; 404s if the id is unknown. */
41
+ export declare function fetchSubmissionRow(db: D1Database, id: string): Promise<SubmissionRowWithForm>;
42
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../../src/modules/forms/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAI/B,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,mFAAmF;AACnF,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,2EAA2E;AAC3E,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,YAAY,CAEpE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,CAazD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,qBAAqB,GAAG,gBAAgB,CAc/E;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOtF;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAIhF;AAED,8EAA8E;AAC9E,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,qBAAqB,CAAC,CAWhC"}