@endora-commerce/mod-blog 0.100.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/admin/api/blog-client.d.ts +70 -0
  4. package/dist/admin/api/blog-client.d.ts.map +1 -0
  5. package/dist/admin/api/blog-client.js +137 -0
  6. package/dist/admin/api/blog-client.js.map +1 -0
  7. package/dist/admin/components/CategoryTreeNode.d.ts +25 -0
  8. package/dist/admin/components/CategoryTreeNode.d.ts.map +1 -0
  9. package/dist/admin/components/CategoryTreeNode.js +14 -0
  10. package/dist/admin/components/CategoryTreeNode.js.map +1 -0
  11. package/dist/admin/components/PostStatusBadge.d.ts +7 -0
  12. package/dist/admin/components/PostStatusBadge.d.ts.map +1 -0
  13. package/dist/admin/components/PostStatusBadge.js +16 -0
  14. package/dist/admin/components/PostStatusBadge.js.map +1 -0
  15. package/dist/admin/components/RelatedPostsPicker.d.ts +11 -0
  16. package/dist/admin/components/RelatedPostsPicker.d.ts.map +1 -0
  17. package/dist/admin/components/RelatedPostsPicker.js +77 -0
  18. package/dist/admin/components/RelatedPostsPicker.js.map +1 -0
  19. package/dist/admin/components/RelatedProductsPicker.d.ts +9 -0
  20. package/dist/admin/components/RelatedProductsPicker.d.ts.map +1 -0
  21. package/dist/admin/components/RelatedProductsPicker.js +66 -0
  22. package/dist/admin/components/RelatedProductsPicker.js.map +1 -0
  23. package/dist/admin/components/TagPicker.d.ts +9 -0
  24. package/dist/admin/components/TagPicker.d.ts.map +1 -0
  25. package/dist/admin/components/TagPicker.js +66 -0
  26. package/dist/admin/components/TagPicker.js.map +1 -0
  27. package/dist/admin/index.d.ts +33 -0
  28. package/dist/admin/index.d.ts.map +1 -0
  29. package/dist/admin/index.js +114 -0
  30. package/dist/admin/index.js.map +1 -0
  31. package/dist/admin/pages/BlogCategoryEditor.d.ts +9 -0
  32. package/dist/admin/pages/BlogCategoryEditor.d.ts.map +1 -0
  33. package/dist/admin/pages/BlogCategoryEditor.js +196 -0
  34. package/dist/admin/pages/BlogCategoryEditor.js.map +1 -0
  35. package/dist/admin/pages/BlogCategoryTreePage.d.ts +9 -0
  36. package/dist/admin/pages/BlogCategoryTreePage.d.ts.map +1 -0
  37. package/dist/admin/pages/BlogCategoryTreePage.js +122 -0
  38. package/dist/admin/pages/BlogCategoryTreePage.js.map +1 -0
  39. package/dist/admin/pages/BlogPostEditor.d.ts +9 -0
  40. package/dist/admin/pages/BlogPostEditor.d.ts.map +1 -0
  41. package/dist/admin/pages/BlogPostEditor.js +314 -0
  42. package/dist/admin/pages/BlogPostEditor.js.map +1 -0
  43. package/dist/admin/pages/BlogPostListPage.d.ts +9 -0
  44. package/dist/admin/pages/BlogPostListPage.d.ts.map +1 -0
  45. package/dist/admin/pages/BlogPostListPage.js +75 -0
  46. package/dist/admin/pages/BlogPostListPage.js.map +1 -0
  47. package/dist/admin/pages/BlogTagListPage.d.ts +9 -0
  48. package/dist/admin/pages/BlogTagListPage.d.ts.map +1 -0
  49. package/dist/admin/pages/BlogTagListPage.js +145 -0
  50. package/dist/admin/pages/BlogTagListPage.js.map +1 -0
  51. package/dist/backend/entities/blog-category-language.entity.d.ts +11 -0
  52. package/dist/backend/entities/blog-category-language.entity.d.ts.map +1 -0
  53. package/dist/backend/entities/blog-category-language.entity.js +35 -0
  54. package/dist/backend/entities/blog-category-language.entity.js.map +1 -0
  55. package/dist/backend/entities/blog-category-sales-channel.entity.d.ts +17 -0
  56. package/dist/backend/entities/blog-category-sales-channel.entity.d.ts.map +1 -0
  57. package/dist/backend/entities/blog-category-sales-channel.entity.js +48 -0
  58. package/dist/backend/entities/blog-category-sales-channel.entity.js.map +1 -0
  59. package/dist/backend/entities/blog-category.entity.d.ts +32 -0
  60. package/dist/backend/entities/blog-category.entity.d.ts.map +1 -0
  61. package/dist/backend/entities/blog-category.entity.js +113 -0
  62. package/dist/backend/entities/blog-category.entity.js.map +1 -0
  63. package/dist/backend/entities/blog-post-category.entity.d.ts +10 -0
  64. package/dist/backend/entities/blog-post-category.entity.d.ts.map +1 -0
  65. package/dist/backend/entities/blog-post-category.entity.js +34 -0
  66. package/dist/backend/entities/blog-post-category.entity.js.map +1 -0
  67. package/dist/backend/entities/blog-post-language.entity.d.ts +9 -0
  68. package/dist/backend/entities/blog-post-language.entity.d.ts.map +1 -0
  69. package/dist/backend/entities/blog-post-language.entity.js +33 -0
  70. package/dist/backend/entities/blog-post-language.entity.js.map +1 -0
  71. package/dist/backend/entities/blog-post-related-post.entity.d.ts +15 -0
  72. package/dist/backend/entities/blog-post-related-post.entity.d.ts.map +1 -0
  73. package/dist/backend/entities/blog-post-related-post.entity.js +42 -0
  74. package/dist/backend/entities/blog-post-related-post.entity.js.map +1 -0
  75. package/dist/backend/entities/blog-post-related-product.entity.d.ts +14 -0
  76. package/dist/backend/entities/blog-post-related-product.entity.d.ts.map +1 -0
  77. package/dist/backend/entities/blog-post-related-product.entity.js +41 -0
  78. package/dist/backend/entities/blog-post-related-product.entity.js.map +1 -0
  79. package/dist/backend/entities/blog-post-sales-channel.entity.d.ts +16 -0
  80. package/dist/backend/entities/blog-post-sales-channel.entity.d.ts.map +1 -0
  81. package/dist/backend/entities/blog-post-sales-channel.entity.js +47 -0
  82. package/dist/backend/entities/blog-post-sales-channel.entity.js.map +1 -0
  83. package/dist/backend/entities/blog-post-tag.entity.d.ts +14 -0
  84. package/dist/backend/entities/blog-post-tag.entity.d.ts.map +1 -0
  85. package/dist/backend/entities/blog-post-tag.entity.js +41 -0
  86. package/dist/backend/entities/blog-post-tag.entity.js.map +1 -0
  87. package/dist/backend/entities/blog-post.entity.d.ts +31 -0
  88. package/dist/backend/entities/blog-post.entity.d.ts.map +1 -0
  89. package/dist/backend/entities/blog-post.entity.js +106 -0
  90. package/dist/backend/entities/blog-post.entity.js.map +1 -0
  91. package/dist/backend/entities/blog-tag.entity.d.ts +18 -0
  92. package/dist/backend/entities/blog-tag.entity.d.ts.map +1 -0
  93. package/dist/backend/entities/blog-tag.entity.js +66 -0
  94. package/dist/backend/entities/blog-tag.entity.js.map +1 -0
  95. package/dist/backend/index.d.ts +100 -0
  96. package/dist/backend/index.d.ts.map +1 -0
  97. package/dist/backend/index.js +182 -0
  98. package/dist/backend/index.js.map +1 -0
  99. package/dist/backend/routes.admin.d.ts +12 -0
  100. package/dist/backend/routes.admin.d.ts.map +1 -0
  101. package/dist/backend/routes.admin.js +148 -0
  102. package/dist/backend/routes.admin.js.map +1 -0
  103. package/dist/backend/routes.storefront.d.ts +6 -0
  104. package/dist/backend/routes.storefront.d.ts.map +1 -0
  105. package/dist/backend/routes.storefront.js +72 -0
  106. package/dist/backend/routes.storefront.js.map +1 -0
  107. package/dist/backend/services/blog-asset-references.d.ts +4 -0
  108. package/dist/backend/services/blog-asset-references.d.ts.map +1 -0
  109. package/dist/backend/services/blog-asset-references.js +79 -0
  110. package/dist/backend/services/blog-asset-references.js.map +1 -0
  111. package/dist/backend/services/blog-cache.d.ts +55 -0
  112. package/dist/backend/services/blog-cache.d.ts.map +1 -0
  113. package/dist/backend/services/blog-cache.js +111 -0
  114. package/dist/backend/services/blog-cache.js.map +1 -0
  115. package/dist/backend/services/blog-category-service.d.ts +53 -0
  116. package/dist/backend/services/blog-category-service.d.ts.map +1 -0
  117. package/dist/backend/services/blog-category-service.js +446 -0
  118. package/dist/backend/services/blog-category-service.js.map +1 -0
  119. package/dist/backend/services/blog-language-reference.d.ts +18 -0
  120. package/dist/backend/services/blog-language-reference.d.ts.map +1 -0
  121. package/dist/backend/services/blog-language-reference.js +43 -0
  122. package/dist/backend/services/blog-language-reference.js.map +1 -0
  123. package/dist/backend/services/blog-post-service.d.ts +79 -0
  124. package/dist/backend/services/blog-post-service.d.ts.map +1 -0
  125. package/dist/backend/services/blog-post-service.js +539 -0
  126. package/dist/backend/services/blog-post-service.js.map +1 -0
  127. package/dist/backend/services/blog-settings-resolver.d.ts +36 -0
  128. package/dist/backend/services/blog-settings-resolver.d.ts.map +1 -0
  129. package/dist/backend/services/blog-settings-resolver.js +68 -0
  130. package/dist/backend/services/blog-settings-resolver.js.map +1 -0
  131. package/dist/backend/services/blog-slug-collision.d.ts +35 -0
  132. package/dist/backend/services/blog-slug-collision.d.ts.map +1 -0
  133. package/dist/backend/services/blog-slug-collision.js +84 -0
  134. package/dist/backend/services/blog-slug-collision.js.map +1 -0
  135. package/dist/backend/services/blog-storefront-resolver.d.ts +107 -0
  136. package/dist/backend/services/blog-storefront-resolver.d.ts.map +1 -0
  137. package/dist/backend/services/blog-storefront-resolver.js +492 -0
  138. package/dist/backend/services/blog-storefront-resolver.js.map +1 -0
  139. package/dist/backend/services/blog-tag-service.d.ts +45 -0
  140. package/dist/backend/services/blog-tag-service.d.ts.map +1 -0
  141. package/dist/backend/services/blog-tag-service.js +198 -0
  142. package/dist/backend/services/blog-tag-service.js.map +1 -0
  143. package/dist/backend/services/seed-default-category.d.ts +25 -0
  144. package/dist/backend/services/seed-default-category.d.ts.map +1 -0
  145. package/dist/backend/services/seed-default-category.js +48 -0
  146. package/dist/backend/services/seed-default-category.js.map +1 -0
  147. package/dist/backend/services/seed-roles.d.ts +41 -0
  148. package/dist/backend/services/seed-roles.d.ts.map +1 -0
  149. package/dist/backend/services/seed-roles.js +98 -0
  150. package/dist/backend/services/seed-roles.js.map +1 -0
  151. package/dist/manifest.d.ts +226 -0
  152. package/dist/manifest.d.ts.map +1 -0
  153. package/dist/manifest.js +236 -0
  154. package/dist/manifest.js.map +1 -0
  155. package/dist/migrations/20260506T081055_blog_init.d.ts +33 -0
  156. package/dist/migrations/20260506T081055_blog_init.d.ts.map +1 -0
  157. package/dist/migrations/20260506T081055_blog_init.js +260 -0
  158. package/dist/migrations/20260506T081055_blog_init.js.map +1 -0
  159. package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts +6 -0
  160. package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts.map +1 -0
  161. package/dist/migrations/20260903T101744_blog_namespace_block_names.js +58 -0
  162. package/dist/migrations/20260903T101744_blog_namespace_block_names.js.map +1 -0
  163. package/dist/migrations/index.d.ts +34 -0
  164. package/dist/migrations/index.d.ts.map +1 -0
  165. package/dist/migrations/index.js +37 -0
  166. package/dist/migrations/index.js.map +1 -0
  167. package/docs/blog/index.md +199 -0
  168. package/i18n/en.json +166 -0
  169. package/i18n/pl.json +166 -0
  170. package/package.json +108 -0
  171. package/tailwind.css +14 -0
@@ -0,0 +1,539 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { DictionaryReferenceError, ERROR_CODES, } from '@endora-commerce/contracts';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ import { assertSlugAvailable } from './blog-slug-collision.js';
5
+ /**
6
+ * BlogPostService — feature 016 / T033.
7
+ *
8
+ * Owns the full BlogPost CRUD + lifecycle + relations contract documented
9
+ * in `contracts/blog-admin-http.contract.md`. The service runs every
10
+ * write inside an `em.transactional(...)` block; the slug-collision
11
+ * helper acquires per-(channel, slug) advisory locks so a concurrent
12
+ * write cannot squeeze a duplicate past the partial unique indexes.
13
+ *
14
+ * The status state machine is documented in `data-model.md` § State
15
+ * transitions: draft → published (sets published_at if null), published
16
+ * → draft (preserves published_at), published ↔ archived. softDelete
17
+ * additionally detaches every blog_post_related_posts row where
18
+ * related_post_id = id (FR-028 / R5) so deleting an article does not
19
+ * orphan a parent post's strip — the storefront just sees a shorter
20
+ * list and the admin's confirmation dialog has already surfaced the
21
+ * affected parents.
22
+ */
23
+ export class BlogPostService {
24
+ emFactory;
25
+ cache;
26
+ dictionaryValidator;
27
+ constructor(emFactory, cache, dictionaryValidator) {
28
+ this.emFactory = emFactory;
29
+ this.cache = cache;
30
+ this.dictionaryValidator = dictionaryValidator;
31
+ }
32
+ // ────────────────────────────────────────────────────────────────────
33
+ // CRUD
34
+ // ────────────────────────────────────────────────────────────────────
35
+ async list(filters) {
36
+ const page = Math.max(1, filters.page ?? 1);
37
+ const perPage = Math.min(100, Math.max(1, filters.perPage ?? 20));
38
+ const em = this.emFactory();
39
+ const where = ['p.deleted_at is null'];
40
+ const params = [];
41
+ if (filters.q) {
42
+ where.push(`(p.name::text ilike ? or p.slug ilike ?)`);
43
+ params.push(`%${filters.q}%`, `%${filters.q}%`);
44
+ }
45
+ if (filters.status) {
46
+ where.push('p.status = ?');
47
+ params.push(filters.status);
48
+ }
49
+ if (filters.salesChannelId) {
50
+ where.push('exists (select 1 from blog_post_sales_channels s where s.blog_post_id = p.id and s.sales_channel_id = ?)');
51
+ params.push(filters.salesChannelId);
52
+ }
53
+ if (filters.categoryId) {
54
+ where.push('exists (select 1 from blog_post_categories c where c.blog_post_id = p.id and c.blog_category_id = ?)');
55
+ params.push(filters.categoryId);
56
+ }
57
+ if (filters.tagId) {
58
+ where.push('exists (select 1 from blog_post_tags t where t.blog_post_id = p.id and t.blog_tag_id = ?)');
59
+ params.push(filters.tagId);
60
+ }
61
+ if (filters.language) {
62
+ where.push('exists (select 1 from blog_post_languages l where l.blog_post_id = p.id and l.language = ?)');
63
+ params.push(filters.language);
64
+ }
65
+ const whereSql = where.length > 0 ? `where ${where.join(' and ')}` : '';
66
+ const totalRows = (await em.execute(`select count(*)::int as n from blog_posts p ${whereSql}`, params));
67
+ const totalItems = totalRows[0]?.n ?? 0;
68
+ const offset = (page - 1) * perPage;
69
+ const rows = (await em.execute(`select p.* from blog_posts p
70
+ ${whereSql}
71
+ order by p.updated_at desc
72
+ limit ${perPage} offset ${offset}`, params));
73
+ const data = await Promise.all(rows.map((r) => this.toSummary(r)));
74
+ return {
75
+ data,
76
+ pagination: {
77
+ page,
78
+ perPage,
79
+ totalPages: Math.max(1, Math.ceil(totalItems / perPage)),
80
+ totalItems,
81
+ },
82
+ };
83
+ }
84
+ async create(input) {
85
+ const em = this.emFactory();
86
+ const id = randomUUID();
87
+ const now = new Date();
88
+ return em.transactional(async (tx) => {
89
+ // Slug-collision guard runs first so we don't pollute the post row
90
+ // when the slug is already taken.
91
+ await assertSlugAvailable(tx, {
92
+ slug: input.slug,
93
+ salesChannelIds: input.salesChannelIds,
94
+ kind: 'post',
95
+ });
96
+ await this.validateLanguages(input.languages);
97
+ // Auto-fill the seeded Default category when none supplied (FR-011).
98
+ let categoryIds = input.categoryIds ?? [];
99
+ if (categoryIds.length === 0) {
100
+ const defaultRows = (await tx.execute(`select id::text as id from blog_categories where is_system = true and deleted_at is null limit 1`));
101
+ if (defaultRows.length === 0) {
102
+ throw new HttpError(500, ERROR_CODES.INTERNAL, 'Seeded Default blog category is missing. Run the boot reconciler.');
103
+ }
104
+ categoryIds = [defaultRows[0].id];
105
+ }
106
+ await tx.execute(`insert into blog_posts
107
+ (id, name, slug, active, status, published_at, description,
108
+ meta_title, meta_description, meta_keywords, content,
109
+ version, created_at, updated_at)
110
+ values (?, ?::jsonb, ?, ?, 'draft', null, ?, ?::jsonb, ?::jsonb, ?::jsonb, ?::jsonb, 1, ?, ?)`, [
111
+ id,
112
+ JSON.stringify(input.name),
113
+ input.slug,
114
+ input.active ?? true,
115
+ input.description ?? null,
116
+ input.metaTitle ? JSON.stringify(input.metaTitle) : null,
117
+ input.metaDescription ? JSON.stringify(input.metaDescription) : null,
118
+ input.metaKeywords ? JSON.stringify(input.metaKeywords) : null,
119
+ input.content ? JSON.stringify(input.content) : '{}',
120
+ now,
121
+ now,
122
+ ]);
123
+ for (const channelId of input.salesChannelIds) {
124
+ await tx.execute(`insert into blog_post_sales_channels (blog_post_id, sales_channel_id, slug)
125
+ values (?, ?, ?)`, [id, channelId, input.slug]);
126
+ }
127
+ for (const language of input.languages) {
128
+ await tx.execute(`insert into blog_post_languages (blog_post_id, language) values (?, ?)`, [id, language]);
129
+ }
130
+ for (const categoryId of categoryIds) {
131
+ await tx.execute(`insert into blog_post_categories (blog_post_id, blog_category_id) values (?, ?)`, [id, categoryId]);
132
+ }
133
+ for (let i = 0; i < (input.tagIds ?? []).length; i++) {
134
+ await tx.execute(`insert into blog_post_tags (blog_post_id, blog_tag_id, position) values (?, ?, ?)`, [id, input.tagIds[i], i]);
135
+ }
136
+ return this.getByIdInTx(tx, id);
137
+ });
138
+ }
139
+ async getById(id) {
140
+ return this.getByIdInTx(this.emFactory(), id);
141
+ }
142
+ async patch(id, input) {
143
+ const em = this.emFactory();
144
+ return em.transactional(async (tx) => {
145
+ const existing = await this.findRow(tx, id);
146
+ if (!existing) {
147
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
148
+ }
149
+ this.assertVersion(existing.version, input.version);
150
+ const slugChange = input.slug !== undefined && input.slug !== existing.slug;
151
+ const channelChange = input.salesChannelIds !== undefined;
152
+ // Slug-collision guard whenever slug or channel scope changes.
153
+ if (slugChange || channelChange) {
154
+ const targetSlug = input.slug ?? existing.slug;
155
+ const targetChannels = input.salesChannelIds ?? (await this.loadChannelIds(tx, id));
156
+ await assertSlugAvailable(tx, {
157
+ slug: targetSlug,
158
+ salesChannelIds: targetChannels,
159
+ kind: 'post',
160
+ excludeId: id,
161
+ });
162
+ }
163
+ const sets = ['version = version + 1', 'updated_at = now()'];
164
+ const params = [];
165
+ if (input.name !== undefined) {
166
+ sets.push('name = ?::jsonb');
167
+ params.push(JSON.stringify(input.name));
168
+ }
169
+ if (input.slug !== undefined) {
170
+ sets.push('slug = ?');
171
+ params.push(input.slug);
172
+ }
173
+ if (input.active !== undefined) {
174
+ sets.push('active = ?');
175
+ params.push(input.active);
176
+ }
177
+ if (input.description !== undefined) {
178
+ sets.push('description = ?');
179
+ params.push(input.description);
180
+ }
181
+ if (input.metaTitle !== undefined) {
182
+ sets.push('meta_title = ?::jsonb');
183
+ params.push(input.metaTitle ? JSON.stringify(input.metaTitle) : null);
184
+ }
185
+ if (input.metaDescription !== undefined) {
186
+ sets.push('meta_description = ?::jsonb');
187
+ params.push(input.metaDescription ? JSON.stringify(input.metaDescription) : null);
188
+ }
189
+ if (input.metaKeywords !== undefined) {
190
+ sets.push('meta_keywords = ?::jsonb');
191
+ params.push(input.metaKeywords ? JSON.stringify(input.metaKeywords) : null);
192
+ }
193
+ if (sets.length > 2) {
194
+ // there's at least one field beyond the two bookkeeping sets
195
+ params.push(id);
196
+ await tx.execute(`update blog_posts set ${sets.join(', ')} where id = ?`, params);
197
+ }
198
+ // Sync the slug mirror onto the scope rows whenever the slug changes.
199
+ if (slugChange) {
200
+ await tx.execute(`update blog_post_sales_channels set slug = ? where blog_post_id = ?`, [input.slug, id]);
201
+ }
202
+ // Sync sales-channel scope.
203
+ if (channelChange) {
204
+ await this.replaceChannels(tx, id, input.salesChannelIds, input.slug ?? existing.slug);
205
+ }
206
+ // Sync language scope.
207
+ if (input.languages !== undefined) {
208
+ const currentLanguages = await this.loadLanguages(tx, id);
209
+ await this.validateLanguages(input.languages, currentLanguages);
210
+ await this.replaceLanguages(tx, id, input.languages);
211
+ }
212
+ // Sync categories.
213
+ if (input.categoryIds !== undefined) {
214
+ const catIds = input.categoryIds.length > 0
215
+ ? input.categoryIds
216
+ : await this.fallbackToDefaultCategory(tx);
217
+ await this.replaceCategories(tx, id, catIds);
218
+ }
219
+ // Sync tags (ordered).
220
+ if (input.tagIds !== undefined) {
221
+ await this.replaceTags(tx, id, input.tagIds);
222
+ }
223
+ const detail = await this.getByIdInTx(tx, id);
224
+ await this.invalidateCacheForPost(existing.slug, detail.slug);
225
+ return detail;
226
+ });
227
+ }
228
+ async setContent(id, input) {
229
+ const em = this.emFactory();
230
+ return em.transactional(async (tx) => {
231
+ const existing = await this.findRow(tx, id);
232
+ if (!existing) {
233
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
234
+ }
235
+ this.assertVersion(existing.version, input.version);
236
+ await tx.execute(`update blog_posts
237
+ set content = ?::jsonb,
238
+ version = version + 1,
239
+ updated_at = now()
240
+ where id = ?`, [JSON.stringify(input.content), id]);
241
+ const detail = await this.getByIdInTx(tx, id);
242
+ await this.invalidateCacheForPost(existing.slug, detail.slug);
243
+ return detail;
244
+ });
245
+ }
246
+ async setTags(id, tagIds, version) {
247
+ const em = this.emFactory();
248
+ return em.transactional(async (tx) => {
249
+ const existing = await this.findRow(tx, id);
250
+ if (!existing) {
251
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
252
+ }
253
+ this.assertVersion(existing.version, version);
254
+ await this.replaceTags(tx, id, tagIds);
255
+ await tx.execute(`update blog_posts set version = version + 1, updated_at = now() where id = ?`, [id]);
256
+ const detail = await this.getByIdInTx(tx, id);
257
+ await this.invalidateCacheForPost(existing.slug, detail.slug);
258
+ return detail;
259
+ });
260
+ }
261
+ async setRelatedPosts(id, relatedPostIds, version) {
262
+ if (relatedPostIds.includes(id)) {
263
+ throw new HttpError(422, ERROR_CODES.BLOG_RELATED_POST_SELF_REFERENCE, 'A post cannot list itself as a Related Post.');
264
+ }
265
+ const em = this.emFactory();
266
+ return em.transactional(async (tx) => {
267
+ const existing = await this.findRow(tx, id);
268
+ if (!existing) {
269
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
270
+ }
271
+ this.assertVersion(existing.version, version);
272
+ await tx.execute(`delete from blog_post_related_posts where parent_post_id = ?`, [id]);
273
+ for (let i = 0; i < relatedPostIds.length; i++) {
274
+ await tx.execute(`insert into blog_post_related_posts (parent_post_id, related_post_id, position)
275
+ values (?, ?, ?)`, [id, relatedPostIds[i], i]);
276
+ }
277
+ await tx.execute(`update blog_posts set version = version + 1, updated_at = now() where id = ?`, [id]);
278
+ const detail = await this.getByIdInTx(tx, id);
279
+ await this.invalidateCacheForPost(existing.slug, detail.slug);
280
+ return detail;
281
+ });
282
+ }
283
+ async setRelatedProducts(id, productIds, version) {
284
+ const em = this.emFactory();
285
+ return em.transactional(async (tx) => {
286
+ const existing = await this.findRow(tx, id);
287
+ if (!existing) {
288
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
289
+ }
290
+ this.assertVersion(existing.version, version);
291
+ await tx.execute(`delete from blog_post_related_products where blog_post_id = ?`, [id]);
292
+ for (let i = 0; i < productIds.length; i++) {
293
+ await tx.execute(`insert into blog_post_related_products (blog_post_id, product_id, position)
294
+ values (?, ?, ?)`, [id, productIds[i], i]);
295
+ }
296
+ await tx.execute(`update blog_posts set version = version + 1, updated_at = now() where id = ?`, [id]);
297
+ const detail = await this.getByIdInTx(tx, id);
298
+ await this.invalidateCacheForPost(existing.slug, detail.slug);
299
+ return detail;
300
+ });
301
+ }
302
+ async publish(id, version) {
303
+ return this.transitionStatus(id, version, 'published', { setPublishedAt: true });
304
+ }
305
+ async unpublish(id, version) {
306
+ return this.transitionStatus(id, version, 'draft', { setPublishedAt: false });
307
+ }
308
+ async archive(id, version) {
309
+ return this.transitionStatus(id, version, 'archived', { setPublishedAt: false });
310
+ }
311
+ async softDelete(id, version) {
312
+ const em = this.emFactory();
313
+ return em.transactional(async (tx) => {
314
+ const existing = await this.findRow(tx, id);
315
+ if (!existing) {
316
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
317
+ }
318
+ this.assertVersion(existing.version, version);
319
+ // Capture parents that reference this post as related so the
320
+ // response can list them (R5).
321
+ const parents = (await tx.execute(`select parent_post_id::text as id from blog_post_related_posts where related_post_id = ?`, [id]));
322
+ const parentIds = parents.map((p) => p.id);
323
+ // Detach the inbound related-post references atomically.
324
+ if (parentIds.length > 0) {
325
+ await tx.execute(`delete from blog_post_related_posts where related_post_id = ?`, [id]);
326
+ }
327
+ // Soft-delete the post + sync the deleted_at mirror onto the
328
+ // scope rows so the partial-unique slug index releases the slug.
329
+ await tx.execute(`update blog_posts set deleted_at = now(), updated_at = now() where id = ?`, [id]);
330
+ await tx.execute(`update blog_post_sales_channels set deleted_at = now() where blog_post_id = ?`, [id]);
331
+ await this.invalidateCacheForPost(existing.slug, existing.slug);
332
+ return { detachedFromParents: parentIds };
333
+ });
334
+ }
335
+ async getInboundReferences(id) {
336
+ const em = this.emFactory();
337
+ const rows = (await em.execute(`select p.id::text as id, p.name, p.slug
338
+ from blog_post_related_posts r
339
+ join blog_posts p on p.id = r.parent_post_id
340
+ where r.related_post_id = ?
341
+ and p.deleted_at is null
342
+ order by p.slug`, [id]));
343
+ return {
344
+ asRelatedPostBy: rows.map((r) => ({ id: r.id, name: r.name, slug: r.slug })),
345
+ };
346
+ }
347
+ // ────────────────────────────────────────────────────────────────────
348
+ // Internals
349
+ // ────────────────────────────────────────────────────────────────────
350
+ async transitionStatus(id, version, nextStatus, opts) {
351
+ const em = this.emFactory();
352
+ const detail = await em.transactional(async (tx) => {
353
+ const existing = await this.findRow(tx, id);
354
+ if (!existing) {
355
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
356
+ }
357
+ this.assertVersion(existing.version, version);
358
+ const sets = ['status = ?', 'version = version + 1', 'updated_at = now()'];
359
+ const params = [nextStatus];
360
+ if (opts.setPublishedAt && existing.published_at === null) {
361
+ sets.push('published_at = now()');
362
+ }
363
+ params.push(id);
364
+ await tx.execute(`update blog_posts set ${sets.join(', ')} where id = ?`, params);
365
+ return this.getByIdInTx(tx, id);
366
+ });
367
+ // Invalidate AFTER the transaction commits. Doing it inside the
368
+ // transaction (before commit) opens a race: a concurrent storefront read
369
+ // can repopulate the cache with the pre-commit row — e.g. the still-draft
370
+ // version excluded from published listings — and that stale entry then
371
+ // survives until the TTL, so a freshly published post stays invisible on
372
+ // the blog list. Publishing is exactly the transition users report here.
373
+ await this.invalidateCacheForPost(detail.slug, detail.slug);
374
+ return detail;
375
+ }
376
+ async findRow(em, id) {
377
+ const rows = (await em.execute(`select * from blog_posts where id = ? and deleted_at is null limit 1`, [id]));
378
+ return rows[0] ?? null;
379
+ }
380
+ assertVersion(have, sent) {
381
+ if (sent !== undefined && sent !== have) {
382
+ throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, `Blog post was updated concurrently (have v${have}, request v${sent}).`);
383
+ }
384
+ }
385
+ async getByIdInTx(em, id) {
386
+ const row = await this.findRow(em, id);
387
+ if (!row) {
388
+ throw new HttpError(404, ERROR_CODES.BLOG_POST_NOT_FOUND, 'Blog post not found.');
389
+ }
390
+ const [channels, languages, categories, tags, relatedPosts, relatedProducts] = (await Promise.all([
391
+ em.execute(`select sales_channel_id::text as id from blog_post_sales_channels where blog_post_id = ? and deleted_at is null`, [id]),
392
+ em.execute(`select language from blog_post_languages where blog_post_id = ?`, [id]),
393
+ em.execute(`select blog_category_id::text as id from blog_post_categories where blog_post_id = ?`, [id]),
394
+ em.execute(`select t.id::text as id, t.code, bpt.position
395
+ from blog_post_tags bpt
396
+ join blog_tags t on t.id = bpt.blog_tag_id and t.deleted_at is null
397
+ where bpt.blog_post_id = ?
398
+ order by bpt.position`, [id]),
399
+ em.execute(`select related_post_id::text as id from blog_post_related_posts where parent_post_id = ? order by position`, [id]),
400
+ em.execute(`select product_id::text as id from blog_post_related_products where blog_post_id = ? order by position`, [id]),
401
+ ]));
402
+ const detail = {
403
+ id: row.id,
404
+ name: row.name,
405
+ slug: row.slug,
406
+ active: row.active,
407
+ status: row.status,
408
+ publishedAt: this.toIso(row.published_at),
409
+ description: row.description,
410
+ metaTitle: row.meta_title,
411
+ metaDescription: row.meta_description,
412
+ metaKeywords: row.meta_keywords,
413
+ meta: null, // legacy field not used; consumers read metaTitle/metaDescription/metaKeywords
414
+ content: this.normaliseContent(row.content),
415
+ version: row.version,
416
+ createdAt: this.toIsoNonNull(row.created_at),
417
+ updatedAt: this.toIsoNonNull(row.updated_at),
418
+ salesChannelIds: channels.map((c) => c.id),
419
+ languages: languages.map((l) => l.language),
420
+ categoryIds: categories.map((c) => c.id),
421
+ tagIds: tags.map((t) => t.id),
422
+ tags: tags.map((t) => ({ id: t.id, code: t.code, position: t.position })),
423
+ relatedPostIds: relatedPosts.map((r) => r.id),
424
+ relatedProductIds: relatedProducts.map((p) => p.id),
425
+ };
426
+ return detail;
427
+ }
428
+ async toSummary(row) {
429
+ const em = this.emFactory();
430
+ const [channels, languages, categories, tags] = (await Promise.all([
431
+ em.execute(`select sales_channel_id::text as id from blog_post_sales_channels where blog_post_id = ? and deleted_at is null`, [row.id]),
432
+ em.execute(`select language from blog_post_languages where blog_post_id = ?`, [row.id]),
433
+ em.execute(`select blog_category_id::text as id from blog_post_categories where blog_post_id = ?`, [row.id]),
434
+ em.execute(`select blog_tag_id::text as id from blog_post_tags where blog_post_id = ?`, [row.id]),
435
+ ]));
436
+ return {
437
+ id: row.id,
438
+ name: row.name,
439
+ slug: row.slug,
440
+ status: row.status,
441
+ active: row.active,
442
+ publishedAt: this.toIso(row.published_at),
443
+ description: row.description,
444
+ version: row.version,
445
+ createdAt: this.toIsoNonNull(row.created_at),
446
+ updatedAt: this.toIsoNonNull(row.updated_at),
447
+ salesChannelIds: channels.map((c) => c.id),
448
+ languages: languages.map((l) => l.language),
449
+ categoryIds: categories.map((c) => c.id),
450
+ tagIds: tags.map((t) => t.id),
451
+ };
452
+ }
453
+ normaliseContent(raw) {
454
+ if (!raw || typeof raw !== 'object') {
455
+ return { schema_version: 1, languages: {} };
456
+ }
457
+ return {
458
+ schema_version: typeof raw.schema_version === 'number' ? raw.schema_version : 1,
459
+ languages: raw.languages ?? {},
460
+ };
461
+ }
462
+ toIso(value) {
463
+ if (value === null || value === undefined)
464
+ return null;
465
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
466
+ }
467
+ toIsoNonNull(value) {
468
+ return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
469
+ }
470
+ async loadChannelIds(em, postId) {
471
+ const rows = (await em.execute(`select sales_channel_id::text as id from blog_post_sales_channels where blog_post_id = ? and deleted_at is null`, [postId]));
472
+ return rows.map((r) => r.id);
473
+ }
474
+ async loadLanguages(em, postId) {
475
+ const rows = (await em.execute(`select language from blog_post_languages where blog_post_id = ?`, [postId]));
476
+ return rows.map((r) => r.language);
477
+ }
478
+ async validateLanguages(languages, currentLanguages = []) {
479
+ if (!this.dictionaryValidator)
480
+ return;
481
+ const current = new Set(currentLanguages);
482
+ for (const language of languages) {
483
+ try {
484
+ await this.dictionaryValidator.validateLanguageCode(language, current.has(language) ? 'unchanged' : 'create-or-change');
485
+ }
486
+ catch (err) {
487
+ if (err instanceof DictionaryReferenceError) {
488
+ throw new HttpError(409, err.code, err.code === 'DICTIONARY_ENTRY_INACTIVE'
489
+ ? `Language code ${err.entryCode} is no longer available for blog posts.`
490
+ : `Language code ${err.entryCode} is not recognised.`, [{ path: 'languages', issue: err.code }]);
491
+ }
492
+ throw err;
493
+ }
494
+ }
495
+ }
496
+ async replaceChannels(em, postId, channelIds, slug) {
497
+ await em.execute(`delete from blog_post_sales_channels where blog_post_id = ?`, [postId]);
498
+ for (const id of channelIds) {
499
+ await em.execute(`insert into blog_post_sales_channels (blog_post_id, sales_channel_id, slug) values (?, ?, ?)`, [postId, id, slug]);
500
+ }
501
+ }
502
+ async replaceLanguages(em, postId, languages) {
503
+ await em.execute(`delete from blog_post_languages where blog_post_id = ?`, [postId]);
504
+ for (const lang of languages) {
505
+ await em.execute(`insert into blog_post_languages (blog_post_id, language) values (?, ?)`, [postId, lang]);
506
+ }
507
+ }
508
+ async replaceCategories(em, postId, categoryIds) {
509
+ await em.execute(`delete from blog_post_categories where blog_post_id = ?`, [postId]);
510
+ for (const id of categoryIds) {
511
+ await em.execute(`insert into blog_post_categories (blog_post_id, blog_category_id) values (?, ?)`, [postId, id]);
512
+ }
513
+ }
514
+ async replaceTags(em, postId, tagIds) {
515
+ await em.execute(`delete from blog_post_tags where blog_post_id = ?`, [postId]);
516
+ for (let i = 0; i < tagIds.length; i++) {
517
+ await em.execute(`insert into blog_post_tags (blog_post_id, blog_tag_id, position) values (?, ?, ?)`, [postId, tagIds[i], i]);
518
+ }
519
+ }
520
+ async fallbackToDefaultCategory(em) {
521
+ const rows = (await em.execute(`select id::text as id from blog_categories where is_system = true and deleted_at is null limit 1`));
522
+ if (rows.length === 0) {
523
+ throw new HttpError(500, ERROR_CODES.INTERNAL, 'Seeded Default blog category is missing.');
524
+ }
525
+ return [rows[0].id];
526
+ }
527
+ async invalidateCacheForPost(oldSlug, newSlug) {
528
+ if (!this.cache)
529
+ return;
530
+ // Cache is per-channel; we invalidate all channels coarsely. Future
531
+ // refinement: track the specific channel scope and invalidate only
532
+ // those keys. Until then, the catastrophic invalidation is correct
533
+ // and the rate of writes is low (operator-driven, not customer-driven).
534
+ await this.cache.invalidateAll();
535
+ void oldSlug;
536
+ void newSlug;
537
+ }
538
+ }
539
+ //# sourceMappingURL=blog-post-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-post-service.js","sourceRoot":"","sources":["../../../src/backend/services/blog-post-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Thin port over the Settings module — the only surface the Blog module
4
+ * consumes from feature 004. We keep the dependency narrow on purpose
5
+ * (Constitution I): the resolver does not import the Settings entities.
6
+ *
7
+ * `get<T>` throws when the setting is not registered or its value shape
8
+ * mismatches the schema. The resolver swallows those errors and falls
9
+ * back to the documented default (defence-in-depth: a Settings hiccup
10
+ * must not break the storefront, mirroring the comparisons module's
11
+ * `resolveMaxProducts` pattern).
12
+ */
13
+ export interface SettingsServicePort {
14
+ get<T>(code: string, salesChannelId: string, schema: z.ZodType<T>): Promise<T>;
15
+ }
16
+ export interface ResolvedBlogSettings {
17
+ enabled: boolean;
18
+ urlPrefix: string;
19
+ latestCount: number;
20
+ postsPerPage: number;
21
+ }
22
+ export declare class BlogSettingsResolver {
23
+ private readonly settings;
24
+ constructor(settings: SettingsServicePort);
25
+ getResolved(salesChannelId: string): Promise<ResolvedBlogSettings>;
26
+ private readWithFallback;
27
+ /**
28
+ * Pure validator wired into the Settings module's setter callback for
29
+ * the `blog.url_prefix` key. Refuses an invalid shape, a reserved
30
+ * Next.js segment, or a value already owned by another storefront route
31
+ * (the cross-module owner check is delegated; v1 only enforces the
32
+ * shape + reserved-segments rule).
33
+ */
34
+ static assertValidPrefix(value: unknown): asserts value is string;
35
+ }
36
+ //# sourceMappingURL=blog-settings-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-settings-resolver.d.ts","sourceRoot":"","sources":["../../../src/backend/services/blog-settings-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAChF;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAmBD,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,mBAAmB;IAEpD,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAkC1D,gBAAgB;IAa9B;;;;;;OAMG;IACH,MAAM,CAAC,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM;CAgBlE"}