@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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/admin/api/blog-client.d.ts +70 -0
- package/dist/admin/api/blog-client.d.ts.map +1 -0
- package/dist/admin/api/blog-client.js +137 -0
- package/dist/admin/api/blog-client.js.map +1 -0
- package/dist/admin/components/CategoryTreeNode.d.ts +25 -0
- package/dist/admin/components/CategoryTreeNode.d.ts.map +1 -0
- package/dist/admin/components/CategoryTreeNode.js +14 -0
- package/dist/admin/components/CategoryTreeNode.js.map +1 -0
- package/dist/admin/components/PostStatusBadge.d.ts +7 -0
- package/dist/admin/components/PostStatusBadge.d.ts.map +1 -0
- package/dist/admin/components/PostStatusBadge.js +16 -0
- package/dist/admin/components/PostStatusBadge.js.map +1 -0
- package/dist/admin/components/RelatedPostsPicker.d.ts +11 -0
- package/dist/admin/components/RelatedPostsPicker.d.ts.map +1 -0
- package/dist/admin/components/RelatedPostsPicker.js +77 -0
- package/dist/admin/components/RelatedPostsPicker.js.map +1 -0
- package/dist/admin/components/RelatedProductsPicker.d.ts +9 -0
- package/dist/admin/components/RelatedProductsPicker.d.ts.map +1 -0
- package/dist/admin/components/RelatedProductsPicker.js +66 -0
- package/dist/admin/components/RelatedProductsPicker.js.map +1 -0
- package/dist/admin/components/TagPicker.d.ts +9 -0
- package/dist/admin/components/TagPicker.d.ts.map +1 -0
- package/dist/admin/components/TagPicker.js +66 -0
- package/dist/admin/components/TagPicker.js.map +1 -0
- package/dist/admin/index.d.ts +33 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +114 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/BlogCategoryEditor.d.ts +9 -0
- package/dist/admin/pages/BlogCategoryEditor.d.ts.map +1 -0
- package/dist/admin/pages/BlogCategoryEditor.js +196 -0
- package/dist/admin/pages/BlogCategoryEditor.js.map +1 -0
- package/dist/admin/pages/BlogCategoryTreePage.d.ts +9 -0
- package/dist/admin/pages/BlogCategoryTreePage.d.ts.map +1 -0
- package/dist/admin/pages/BlogCategoryTreePage.js +122 -0
- package/dist/admin/pages/BlogCategoryTreePage.js.map +1 -0
- package/dist/admin/pages/BlogPostEditor.d.ts +9 -0
- package/dist/admin/pages/BlogPostEditor.d.ts.map +1 -0
- package/dist/admin/pages/BlogPostEditor.js +314 -0
- package/dist/admin/pages/BlogPostEditor.js.map +1 -0
- package/dist/admin/pages/BlogPostListPage.d.ts +9 -0
- package/dist/admin/pages/BlogPostListPage.d.ts.map +1 -0
- package/dist/admin/pages/BlogPostListPage.js +75 -0
- package/dist/admin/pages/BlogPostListPage.js.map +1 -0
- package/dist/admin/pages/BlogTagListPage.d.ts +9 -0
- package/dist/admin/pages/BlogTagListPage.d.ts.map +1 -0
- package/dist/admin/pages/BlogTagListPage.js +145 -0
- package/dist/admin/pages/BlogTagListPage.js.map +1 -0
- package/dist/backend/entities/blog-category-language.entity.d.ts +11 -0
- package/dist/backend/entities/blog-category-language.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-category-language.entity.js +35 -0
- package/dist/backend/entities/blog-category-language.entity.js.map +1 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.d.ts +17 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.js +48 -0
- package/dist/backend/entities/blog-category-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/blog-category.entity.d.ts +32 -0
- package/dist/backend/entities/blog-category.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-category.entity.js +113 -0
- package/dist/backend/entities/blog-category.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-category.entity.d.ts +10 -0
- package/dist/backend/entities/blog-post-category.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-category.entity.js +34 -0
- package/dist/backend/entities/blog-post-category.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-language.entity.d.ts +9 -0
- package/dist/backend/entities/blog-post-language.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-language.entity.js +33 -0
- package/dist/backend/entities/blog-post-language.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-related-post.entity.d.ts +15 -0
- package/dist/backend/entities/blog-post-related-post.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-related-post.entity.js +42 -0
- package/dist/backend/entities/blog-post-related-post.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-related-product.entity.d.ts +14 -0
- package/dist/backend/entities/blog-post-related-product.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-related-product.entity.js +41 -0
- package/dist/backend/entities/blog-post-related-product.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.d.ts +16 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.js +47 -0
- package/dist/backend/entities/blog-post-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/blog-post-tag.entity.d.ts +14 -0
- package/dist/backend/entities/blog-post-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post-tag.entity.js +41 -0
- package/dist/backend/entities/blog-post-tag.entity.js.map +1 -0
- package/dist/backend/entities/blog-post.entity.d.ts +31 -0
- package/dist/backend/entities/blog-post.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-post.entity.js +106 -0
- package/dist/backend/entities/blog-post.entity.js.map +1 -0
- package/dist/backend/entities/blog-tag.entity.d.ts +18 -0
- package/dist/backend/entities/blog-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/blog-tag.entity.js +66 -0
- package/dist/backend/entities/blog-tag.entity.js.map +1 -0
- package/dist/backend/index.d.ts +100 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +182 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +12 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +148 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +6 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +72 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/blog-asset-references.d.ts +4 -0
- package/dist/backend/services/blog-asset-references.d.ts.map +1 -0
- package/dist/backend/services/blog-asset-references.js +79 -0
- package/dist/backend/services/blog-asset-references.js.map +1 -0
- package/dist/backend/services/blog-cache.d.ts +55 -0
- package/dist/backend/services/blog-cache.d.ts.map +1 -0
- package/dist/backend/services/blog-cache.js +111 -0
- package/dist/backend/services/blog-cache.js.map +1 -0
- package/dist/backend/services/blog-category-service.d.ts +53 -0
- package/dist/backend/services/blog-category-service.d.ts.map +1 -0
- package/dist/backend/services/blog-category-service.js +446 -0
- package/dist/backend/services/blog-category-service.js.map +1 -0
- package/dist/backend/services/blog-language-reference.d.ts +18 -0
- package/dist/backend/services/blog-language-reference.d.ts.map +1 -0
- package/dist/backend/services/blog-language-reference.js +43 -0
- package/dist/backend/services/blog-language-reference.js.map +1 -0
- package/dist/backend/services/blog-post-service.d.ts +79 -0
- package/dist/backend/services/blog-post-service.d.ts.map +1 -0
- package/dist/backend/services/blog-post-service.js +539 -0
- package/dist/backend/services/blog-post-service.js.map +1 -0
- package/dist/backend/services/blog-settings-resolver.d.ts +36 -0
- package/dist/backend/services/blog-settings-resolver.d.ts.map +1 -0
- package/dist/backend/services/blog-settings-resolver.js +68 -0
- package/dist/backend/services/blog-settings-resolver.js.map +1 -0
- package/dist/backend/services/blog-slug-collision.d.ts +35 -0
- package/dist/backend/services/blog-slug-collision.d.ts.map +1 -0
- package/dist/backend/services/blog-slug-collision.js +84 -0
- package/dist/backend/services/blog-slug-collision.js.map +1 -0
- package/dist/backend/services/blog-storefront-resolver.d.ts +107 -0
- package/dist/backend/services/blog-storefront-resolver.d.ts.map +1 -0
- package/dist/backend/services/blog-storefront-resolver.js +492 -0
- package/dist/backend/services/blog-storefront-resolver.js.map +1 -0
- package/dist/backend/services/blog-tag-service.d.ts +45 -0
- package/dist/backend/services/blog-tag-service.d.ts.map +1 -0
- package/dist/backend/services/blog-tag-service.js +198 -0
- package/dist/backend/services/blog-tag-service.js.map +1 -0
- package/dist/backend/services/seed-default-category.d.ts +25 -0
- package/dist/backend/services/seed-default-category.d.ts.map +1 -0
- package/dist/backend/services/seed-default-category.js +48 -0
- package/dist/backend/services/seed-default-category.js.map +1 -0
- package/dist/backend/services/seed-roles.d.ts +41 -0
- package/dist/backend/services/seed-roles.d.ts.map +1 -0
- package/dist/backend/services/seed-roles.js +98 -0
- package/dist/backend/services/seed-roles.js.map +1 -0
- package/dist/manifest.d.ts +226 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +236 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260506T081055_blog_init.d.ts +33 -0
- package/dist/migrations/20260506T081055_blog_init.d.ts.map +1 -0
- package/dist/migrations/20260506T081055_blog_init.js +260 -0
- package/dist/migrations/20260506T081055_blog_init.js.map +1 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts +6 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.d.ts.map +1 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.js +58 -0
- package/dist/migrations/20260903T101744_blog_namespace_block_names.js.map +1 -0
- package/dist/migrations/index.d.ts +34 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +37 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/blog/index.md +199 -0
- package/i18n/en.json +166 -0
- package/i18n/pl.json +166 -0
- package/package.json +108 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { BlogCacheService } from './blog-cache.js';
|
|
2
|
+
function toChannelRow(channel) {
|
|
3
|
+
return { id: channel.id, code: channel.code, default_language: channel.defaultLanguage };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* BlogStorefrontResolver — feature 016 / T051.
|
|
7
|
+
*
|
|
8
|
+
* Three read endpoints behind `/api/v1/blog/**`:
|
|
9
|
+
*
|
|
10
|
+
* getIndex → BlogIndexResponse (latest N + first-level categories)
|
|
11
|
+
* getBySlug → BlogBySlugResponse discriminated by `kind`
|
|
12
|
+
* getTagByCode → BlogTagByCodeResponse paginated
|
|
13
|
+
*
|
|
14
|
+
* Every method:
|
|
15
|
+
* - reads the channel-resolved blog settings via BlogSettingsResolver;
|
|
16
|
+
* - returns null (the route maps to 404) when blog.enabled = false;
|
|
17
|
+
* - applies the platform-wide language-fallback rule (feature 005)
|
|
18
|
+
* via the `pickLanguage` helper;
|
|
19
|
+
* - wraps results in BlogCacheService when Redis is wired.
|
|
20
|
+
*/
|
|
21
|
+
export class BlogStorefrontResolver {
|
|
22
|
+
emFactory;
|
|
23
|
+
settingsResolver;
|
|
24
|
+
deps;
|
|
25
|
+
cache;
|
|
26
|
+
constructor(emFactory, settingsResolver, deps = {}, cache) {
|
|
27
|
+
this.emFactory = emFactory;
|
|
28
|
+
this.settingsResolver = settingsResolver;
|
|
29
|
+
this.deps = deps;
|
|
30
|
+
this.cache = cache;
|
|
31
|
+
}
|
|
32
|
+
async getIndex(input) {
|
|
33
|
+
const em = this.emFactory();
|
|
34
|
+
const channel = toChannelRow(input.resolvedChannel);
|
|
35
|
+
const language = input.language ?? channel.default_language;
|
|
36
|
+
const settings = await this.settingsResolver.getResolved(channel.id);
|
|
37
|
+
if (!settings.enabled)
|
|
38
|
+
return null;
|
|
39
|
+
const cacheKey = BlogCacheService.composeIndexKey(channel.code, language);
|
|
40
|
+
if (this.cache) {
|
|
41
|
+
const cached = await this.cache.get(cacheKey);
|
|
42
|
+
if (cached)
|
|
43
|
+
return cached;
|
|
44
|
+
}
|
|
45
|
+
const latestRows = (await em.execute(`select p.*
|
|
46
|
+
from blog_posts p
|
|
47
|
+
join blog_post_sales_channels psc on psc.blog_post_id = p.id
|
|
48
|
+
where p.deleted_at is null
|
|
49
|
+
and p.active = true
|
|
50
|
+
and p.status = 'published'
|
|
51
|
+
and psc.sales_channel_id = ?
|
|
52
|
+
and psc.deleted_at is null
|
|
53
|
+
order by p.published_at desc nulls last, p.created_at desc
|
|
54
|
+
limit ?`, [channel.id, settings.latestCount]));
|
|
55
|
+
const topLevelRows = (await em.execute(`select c.*
|
|
56
|
+
from blog_categories c
|
|
57
|
+
join blog_category_sales_channels csc on csc.blog_category_id = c.id
|
|
58
|
+
where c.deleted_at is null
|
|
59
|
+
and c.enabled = true
|
|
60
|
+
and c.parent_id is null
|
|
61
|
+
and csc.sales_channel_id = ?
|
|
62
|
+
and csc.deleted_at is null
|
|
63
|
+
order by c.position asc, c.slug asc`, [channel.id]));
|
|
64
|
+
const channelDefault = channel.default_language;
|
|
65
|
+
const latestPosts = await Promise.all(latestRows.map((r) => this.toPostCard(em, r, language, channelDefault)));
|
|
66
|
+
const topLevelCategories = await Promise.all(topLevelRows.map((r) => this.toCategoryTile(r, language, channelDefault)));
|
|
67
|
+
const out = {
|
|
68
|
+
urlPrefix: settings.urlPrefix,
|
|
69
|
+
latestPosts,
|
|
70
|
+
topLevelCategories,
|
|
71
|
+
};
|
|
72
|
+
if (this.cache)
|
|
73
|
+
await this.cache.set(cacheKey, out);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
async getBySlug(slug, input) {
|
|
77
|
+
const em = this.emFactory();
|
|
78
|
+
const channel = toChannelRow(input.resolvedChannel);
|
|
79
|
+
const language = input.language ?? channel.default_language;
|
|
80
|
+
const settings = await this.settingsResolver.getResolved(channel.id);
|
|
81
|
+
if (!settings.enabled)
|
|
82
|
+
return null;
|
|
83
|
+
const channelDefault = channel.default_language;
|
|
84
|
+
// Probe the post side first — by-(channel, slug) covered by the partial-unique index.
|
|
85
|
+
const postRows = (await em.execute(`select p.*
|
|
86
|
+
from blog_posts p
|
|
87
|
+
join blog_post_sales_channels psc on psc.blog_post_id = p.id
|
|
88
|
+
where p.deleted_at is null
|
|
89
|
+
and p.active = true
|
|
90
|
+
and p.status = 'published'
|
|
91
|
+
and psc.sales_channel_id = ?
|
|
92
|
+
and psc.slug = ?
|
|
93
|
+
and psc.deleted_at is null
|
|
94
|
+
limit 1`, [channel.id, slug]));
|
|
95
|
+
if (postRows[0]) {
|
|
96
|
+
const post = postRows[0];
|
|
97
|
+
const cacheKey = BlogCacheService.composePostKey(channel.code, language, slug);
|
|
98
|
+
if (this.cache) {
|
|
99
|
+
const cached = await this.cache.get(cacheKey);
|
|
100
|
+
if (cached)
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
const resolved = await this.toResolvedPost(em, post, channel, language, channelDefault, settings.urlPrefix);
|
|
104
|
+
if (resolved === null)
|
|
105
|
+
return null;
|
|
106
|
+
const payload = { kind: 'post', post: resolved };
|
|
107
|
+
if (this.cache)
|
|
108
|
+
await this.cache.set(cacheKey, payload);
|
|
109
|
+
return payload;
|
|
110
|
+
}
|
|
111
|
+
// Probe the category side.
|
|
112
|
+
const categoryRows = (await em.execute(`select c.*
|
|
113
|
+
from blog_categories c
|
|
114
|
+
join blog_category_sales_channels csc on csc.blog_category_id = c.id
|
|
115
|
+
where c.deleted_at is null
|
|
116
|
+
and c.enabled = true
|
|
117
|
+
and csc.sales_channel_id = ?
|
|
118
|
+
and csc.slug = ?
|
|
119
|
+
and csc.deleted_at is null
|
|
120
|
+
limit 1`, [channel.id, slug]));
|
|
121
|
+
if (categoryRows[0]) {
|
|
122
|
+
const category = categoryRows[0];
|
|
123
|
+
const page = Math.max(1, input.page ?? 1);
|
|
124
|
+
const cacheKey = BlogCacheService.composeCategoryKey(channel.code, language, slug, page);
|
|
125
|
+
if (this.cache) {
|
|
126
|
+
const cached = await this.cache.get(cacheKey);
|
|
127
|
+
if (cached)
|
|
128
|
+
return cached;
|
|
129
|
+
}
|
|
130
|
+
const offset = (page - 1) * settings.postsPerPage;
|
|
131
|
+
const totalRows = (await em.execute(`select count(*)::int as n
|
|
132
|
+
from blog_posts p
|
|
133
|
+
join blog_post_categories pc on pc.blog_post_id = p.id
|
|
134
|
+
join blog_post_sales_channels psc on psc.blog_post_id = p.id
|
|
135
|
+
where p.deleted_at is null
|
|
136
|
+
and p.active = true
|
|
137
|
+
and p.status = 'published'
|
|
138
|
+
and pc.blog_category_id = ?
|
|
139
|
+
and psc.sales_channel_id = ?
|
|
140
|
+
and psc.deleted_at is null`, [category.id, channel.id]));
|
|
141
|
+
const totalItems = totalRows[0]?.n ?? 0;
|
|
142
|
+
const pageRows = (await em.execute(`select p.*
|
|
143
|
+
from blog_posts p
|
|
144
|
+
join blog_post_categories pc on pc.blog_post_id = p.id
|
|
145
|
+
join blog_post_sales_channels psc on psc.blog_post_id = p.id
|
|
146
|
+
where p.deleted_at is null
|
|
147
|
+
and p.active = true
|
|
148
|
+
and p.status = 'published'
|
|
149
|
+
and pc.blog_category_id = ?
|
|
150
|
+
and psc.sales_channel_id = ?
|
|
151
|
+
and psc.deleted_at is null
|
|
152
|
+
order by p.published_at desc nulls last, p.created_at desc
|
|
153
|
+
limit ${settings.postsPerPage} offset ${offset}`, [category.id, channel.id]));
|
|
154
|
+
const childRows = (await em.execute(`select c.*
|
|
155
|
+
from blog_categories c
|
|
156
|
+
join blog_category_sales_channels csc on csc.blog_category_id = c.id
|
|
157
|
+
where c.deleted_at is null
|
|
158
|
+
and c.enabled = true
|
|
159
|
+
and c.parent_id = ?
|
|
160
|
+
and csc.sales_channel_id = ?
|
|
161
|
+
and csc.deleted_at is null
|
|
162
|
+
order by c.position asc, c.slug asc`, [category.id, channel.id]));
|
|
163
|
+
const resolvedCategory = {
|
|
164
|
+
id: category.id,
|
|
165
|
+
name: this.pickName(category.name, language, channelDefault) ?? category.slug,
|
|
166
|
+
slug: category.slug,
|
|
167
|
+
mainImageUrl: await this.maybeAssetUrl(category.main_image_asset_id),
|
|
168
|
+
description: this.normaliseDescription(category.description),
|
|
169
|
+
metaTitle: this.pickLanguage(category.meta_title, language, channelDefault),
|
|
170
|
+
metaDescription: this.pickLanguage(category.meta_description, language, channelDefault),
|
|
171
|
+
metaKeywords: this.pickLanguage(category.meta_keywords, language, channelDefault),
|
|
172
|
+
childCategories: await Promise.all(childRows.map((r) => this.toCategoryTile(r, language, channelDefault))),
|
|
173
|
+
breadcrumb: [
|
|
174
|
+
{ name: 'Blog', url: `/${settings.urlPrefix}` },
|
|
175
|
+
{ name: this.pickName(category.name, language, channelDefault) ?? category.slug, url: `/${settings.urlPrefix}/${category.slug}` },
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
const payload = {
|
|
179
|
+
kind: 'category',
|
|
180
|
+
category: resolvedCategory,
|
|
181
|
+
posts: {
|
|
182
|
+
data: await Promise.all(pageRows.map((r) => this.toPostCard(em, r, language, channelDefault))),
|
|
183
|
+
pagination: {
|
|
184
|
+
page,
|
|
185
|
+
perPage: settings.postsPerPage,
|
|
186
|
+
totalPages: Math.max(1, Math.ceil(totalItems / settings.postsPerPage)),
|
|
187
|
+
totalItems,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
if (this.cache)
|
|
192
|
+
await this.cache.set(cacheKey, payload);
|
|
193
|
+
return payload;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
async getTagByCode(code, input) {
|
|
198
|
+
const em = this.emFactory();
|
|
199
|
+
const channel = toChannelRow(input.resolvedChannel);
|
|
200
|
+
const language = input.language ?? channel.default_language;
|
|
201
|
+
const settings = await this.settingsResolver.getResolved(channel.id);
|
|
202
|
+
if (!settings.enabled)
|
|
203
|
+
return null;
|
|
204
|
+
const channelDefault = channel.default_language;
|
|
205
|
+
const tagRows = (await em.execute(`select id::text as id, code, name, description from blog_tags where code = ? and deleted_at is null limit 1`, [code]));
|
|
206
|
+
if (tagRows.length === 0)
|
|
207
|
+
return null;
|
|
208
|
+
const tag = tagRows[0];
|
|
209
|
+
const page = Math.max(1, input.page ?? 1);
|
|
210
|
+
const cacheKey = BlogCacheService.composeTagKey(channel.code, language, code, page);
|
|
211
|
+
if (this.cache) {
|
|
212
|
+
const cached = await this.cache.get(cacheKey);
|
|
213
|
+
if (cached)
|
|
214
|
+
return cached;
|
|
215
|
+
}
|
|
216
|
+
const offset = (page - 1) * settings.postsPerPage;
|
|
217
|
+
const totalRows = (await em.execute(`select count(*)::int as n
|
|
218
|
+
from blog_posts p
|
|
219
|
+
join blog_post_tags pt on pt.blog_post_id = p.id
|
|
220
|
+
join blog_post_sales_channels psc on psc.blog_post_id = p.id
|
|
221
|
+
where p.deleted_at is null
|
|
222
|
+
and p.active = true
|
|
223
|
+
and p.status = 'published'
|
|
224
|
+
and pt.blog_tag_id = ?
|
|
225
|
+
and psc.sales_channel_id = ?
|
|
226
|
+
and psc.deleted_at is null`, [tag.id, channel.id]));
|
|
227
|
+
const totalItems = totalRows[0]?.n ?? 0;
|
|
228
|
+
const pageRows = (await em.execute(`select p.*
|
|
229
|
+
from blog_posts p
|
|
230
|
+
join blog_post_tags pt on pt.blog_post_id = p.id
|
|
231
|
+
join blog_post_sales_channels psc on psc.blog_post_id = p.id
|
|
232
|
+
where p.deleted_at is null
|
|
233
|
+
and p.active = true
|
|
234
|
+
and p.status = 'published'
|
|
235
|
+
and pt.blog_tag_id = ?
|
|
236
|
+
and psc.sales_channel_id = ?
|
|
237
|
+
and psc.deleted_at is null
|
|
238
|
+
order by p.published_at desc nulls last, p.created_at desc
|
|
239
|
+
limit ${settings.postsPerPage} offset ${offset}`, [tag.id, channel.id]));
|
|
240
|
+
const tagName = this.pickLanguage(tag.name, language, channelDefault) ?? tag.code;
|
|
241
|
+
const payload = {
|
|
242
|
+
tag: {
|
|
243
|
+
id: tag.id,
|
|
244
|
+
code: tag.code,
|
|
245
|
+
name: tagName,
|
|
246
|
+
description: this.pickLanguage(tag.description, language, channelDefault),
|
|
247
|
+
},
|
|
248
|
+
posts: {
|
|
249
|
+
data: await Promise.all(pageRows.map((r) => this.toPostCard(em, r, language, channelDefault))),
|
|
250
|
+
pagination: {
|
|
251
|
+
page,
|
|
252
|
+
perPage: settings.postsPerPage,
|
|
253
|
+
totalPages: Math.max(1, Math.ceil(totalItems / settings.postsPerPage)),
|
|
254
|
+
totalItems,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
breadcrumb: [
|
|
258
|
+
{ name: 'Blog', url: `/${settings.urlPrefix}` },
|
|
259
|
+
{ name: tagName, url: `/${settings.urlPrefix}/tag/${tag.code}` },
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
if (this.cache)
|
|
263
|
+
await this.cache.set(cacheKey, payload);
|
|
264
|
+
return payload;
|
|
265
|
+
}
|
|
266
|
+
// ────────────────────────────────────────────────────────────────────
|
|
267
|
+
// Internals
|
|
268
|
+
// ────────────────────────────────────────────────────────────────────
|
|
269
|
+
/**
|
|
270
|
+
* Language-fallback rule (FR-019 / R12 / per feature 005): pick the
|
|
271
|
+
* requested language; fall back to the channel's default language; if
|
|
272
|
+
* neither exists, return null. The caller decides whether `null`
|
|
273
|
+
* means "skip this field" or "404 the page" depending on context.
|
|
274
|
+
*/
|
|
275
|
+
pickLanguage(map, language, channelDefault) {
|
|
276
|
+
if (!map)
|
|
277
|
+
return null;
|
|
278
|
+
if (map[language])
|
|
279
|
+
return map[language] ?? null;
|
|
280
|
+
if (map[channelDefault])
|
|
281
|
+
return map[channelDefault] ?? null;
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Resolve a human-readable name for a published entity. Applies the
|
|
286
|
+
* standard language fallback (requested → channel default) and, when that
|
|
287
|
+
* is exhausted, falls back to the first language actually authored on the
|
|
288
|
+
* entity rather than giving up.
|
|
289
|
+
*
|
|
290
|
+
* This keeps a *published* post/category viewable even when its content
|
|
291
|
+
* was authored only in a language other than the channel's default — the
|
|
292
|
+
* alternative (returning null) made the post-detail resolver 404 while the
|
|
293
|
+
* index/category resolvers still listed the post (via their `?? slug`
|
|
294
|
+
* fallback), an inconsistency that surfaced as "the post is in the list but
|
|
295
|
+
* its page 404s". A published entity is always reachable; only its display
|
|
296
|
+
* language degrades.
|
|
297
|
+
*/
|
|
298
|
+
pickName(map, language, channelDefault) {
|
|
299
|
+
const resolved = this.pickLanguage(map, language, channelDefault);
|
|
300
|
+
if (resolved !== null)
|
|
301
|
+
return resolved;
|
|
302
|
+
if (!map)
|
|
303
|
+
return null;
|
|
304
|
+
for (const value of Object.values(map)) {
|
|
305
|
+
if (value)
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
async toPostCard(em, row, language, channelDefault) {
|
|
311
|
+
// Pull the primary category slug if any (the first one assigned).
|
|
312
|
+
const catRows = (await em.execute(`select c.slug
|
|
313
|
+
from blog_post_categories pc
|
|
314
|
+
join blog_categories c on c.id = pc.blog_category_id
|
|
315
|
+
where pc.blog_post_id = ?
|
|
316
|
+
and c.deleted_at is null
|
|
317
|
+
order by c.is_system desc, c.position asc, c.slug asc
|
|
318
|
+
limit 1`, [row.id]));
|
|
319
|
+
const primaryCategorySlug = catRows[0]?.slug ?? null;
|
|
320
|
+
const name = this.pickName(row.name, language, channelDefault) ?? row.slug;
|
|
321
|
+
const excerpt = this.pickLanguage(row.meta_description, language, channelDefault) ??
|
|
322
|
+
this.firstParagraph(row.content, language, channelDefault);
|
|
323
|
+
return {
|
|
324
|
+
id: row.id,
|
|
325
|
+
slug: row.slug,
|
|
326
|
+
name,
|
|
327
|
+
publishedAt: this.toIso(row.published_at),
|
|
328
|
+
excerpt,
|
|
329
|
+
mainImageUrl: await this.maybeAssetUrl(row.main_image_asset_id),
|
|
330
|
+
primaryCategorySlug,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async toCategoryTile(row, language, channelDefault) {
|
|
334
|
+
return {
|
|
335
|
+
id: row.id,
|
|
336
|
+
name: this.pickName(row.name, language, channelDefault) ?? row.slug,
|
|
337
|
+
slug: row.slug,
|
|
338
|
+
mainImageUrl: await this.maybeAssetUrl(row.main_image_asset_id),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async toResolvedPost(em, post, channel, language, channelDefault, urlPrefix) {
|
|
342
|
+
// A published post is always reachable; fall back to any authored
|
|
343
|
+
// language (then the slug) rather than 404 when the channel default is
|
|
344
|
+
// missing. Keeps the post-detail page consistent with the index/category
|
|
345
|
+
// listings, which already surface the post via their own slug fallback.
|
|
346
|
+
const name = this.pickName(post.name, language, channelDefault) ?? post.slug;
|
|
347
|
+
// Tags (ordered).
|
|
348
|
+
const tagRows = (await em.execute(`select t.id::text as id, t.code, t.name
|
|
349
|
+
from blog_post_tags pt
|
|
350
|
+
join blog_tags t on t.id = pt.blog_tag_id and t.deleted_at is null
|
|
351
|
+
where pt.blog_post_id = ?
|
|
352
|
+
order by pt.position`, [post.id]));
|
|
353
|
+
// Related posts (filtered to available ones in scope).
|
|
354
|
+
const relatedPostRows = (await em.execute(`select p.*
|
|
355
|
+
from blog_post_related_posts r
|
|
356
|
+
join blog_posts p on p.id = r.related_post_id
|
|
357
|
+
join blog_post_sales_channels psc on psc.blog_post_id = p.id
|
|
358
|
+
where r.parent_post_id = ?
|
|
359
|
+
and p.deleted_at is null
|
|
360
|
+
and p.active = true
|
|
361
|
+
and p.status = 'published'
|
|
362
|
+
and psc.sales_channel_id = ?
|
|
363
|
+
and psc.deleted_at is null
|
|
364
|
+
order by r.position`, [post.id, channel.id]));
|
|
365
|
+
const relatedPosts = await Promise.all(relatedPostRows.map((r) => this.toPostCard(em, r, language, channelDefault)));
|
|
366
|
+
// Related products via the optional port.
|
|
367
|
+
const relatedProductIds = (await em.execute(`select product_id::text as id
|
|
368
|
+
from blog_post_related_products
|
|
369
|
+
where blog_post_id = ?
|
|
370
|
+
order by position`, [post.id]));
|
|
371
|
+
const relatedProducts = [];
|
|
372
|
+
if (this.deps.resolveProductCard) {
|
|
373
|
+
for (const r of relatedProductIds) {
|
|
374
|
+
const card = await this.deps.resolveProductCard(r.id, channel.id, language);
|
|
375
|
+
if (card)
|
|
376
|
+
relatedProducts.push(card);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Categories (used in breadcrumb + the resolved-post categories list).
|
|
380
|
+
const categoryRows = (await em.execute(`select c.id::text as id, c.slug, c.name
|
|
381
|
+
from blog_post_categories pc
|
|
382
|
+
join blog_categories c on c.id = pc.blog_category_id and c.deleted_at is null
|
|
383
|
+
where pc.blog_post_id = ?
|
|
384
|
+
order by c.is_system desc, c.position asc, c.slug asc`, [post.id]));
|
|
385
|
+
const categories = categoryRows.map((c) => ({
|
|
386
|
+
id: c.id,
|
|
387
|
+
name: this.pickName(c.name, language, channelDefault) ?? c.slug,
|
|
388
|
+
slug: c.slug,
|
|
389
|
+
}));
|
|
390
|
+
const tags = tagRows.map((t) => ({
|
|
391
|
+
id: t.id,
|
|
392
|
+
code: t.code,
|
|
393
|
+
name: this.pickLanguage(t.name, language, channelDefault) ?? t.code,
|
|
394
|
+
}));
|
|
395
|
+
const breadcrumb = [
|
|
396
|
+
{ name: 'Blog', url: `/${urlPrefix}` },
|
|
397
|
+
...(categories[0]
|
|
398
|
+
? [{ name: categories[0].name, url: `/${urlPrefix}/${categories[0].slug}` }]
|
|
399
|
+
: []),
|
|
400
|
+
{ name, url: `/${urlPrefix}/${post.slug}` },
|
|
401
|
+
];
|
|
402
|
+
return {
|
|
403
|
+
id: post.id,
|
|
404
|
+
name,
|
|
405
|
+
slug: post.slug,
|
|
406
|
+
publishedAt: this.toIso(post.published_at),
|
|
407
|
+
metaTitle: this.pickLanguage(post.meta_title, language, channelDefault),
|
|
408
|
+
metaDescription: this.pickLanguage(post.meta_description, language, channelDefault),
|
|
409
|
+
metaKeywords: this.pickLanguage(post.meta_keywords, language, channelDefault),
|
|
410
|
+
content: this.normaliseContent(post.content),
|
|
411
|
+
tags,
|
|
412
|
+
relatedPosts,
|
|
413
|
+
relatedProducts,
|
|
414
|
+
categories,
|
|
415
|
+
breadcrumb,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async maybeAssetUrl(assetId) {
|
|
419
|
+
if (!assetId)
|
|
420
|
+
return null;
|
|
421
|
+
if (!this.deps.resolveAssetUrl)
|
|
422
|
+
return null;
|
|
423
|
+
return this.deps.resolveAssetUrl(assetId);
|
|
424
|
+
}
|
|
425
|
+
normaliseDescription(raw) {
|
|
426
|
+
if (!raw || typeof raw !== 'object')
|
|
427
|
+
return null;
|
|
428
|
+
return {
|
|
429
|
+
schema_version: typeof raw.schema_version === 'number' ? raw.schema_version : 1,
|
|
430
|
+
languages: raw.languages ?? {},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
normaliseContent(raw) {
|
|
434
|
+
if (!raw || typeof raw !== 'object')
|
|
435
|
+
return { schema_version: 1, languages: {} };
|
|
436
|
+
return {
|
|
437
|
+
schema_version: typeof raw.schema_version === 'number' ? raw.schema_version : 1,
|
|
438
|
+
languages: raw.languages ?? {},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Best-effort excerpt extraction — picks the first text-bearing node
|
|
443
|
+
* in the resolved language's tree. Falls back to the channel default
|
|
444
|
+
* language. Returns null when nothing usable is found.
|
|
445
|
+
*/
|
|
446
|
+
firstParagraph(raw, language, channelDefault) {
|
|
447
|
+
if (!raw || typeof raw !== 'object' || !raw.languages)
|
|
448
|
+
return null;
|
|
449
|
+
const tree = raw.languages[language] ?? raw.languages[channelDefault];
|
|
450
|
+
if (!tree)
|
|
451
|
+
return null;
|
|
452
|
+
return extractText(tree, 160);
|
|
453
|
+
}
|
|
454
|
+
toIso(value) {
|
|
455
|
+
if (value === null || value === undefined)
|
|
456
|
+
return null;
|
|
457
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Recursively walks a Page Builder tree node and collects up to `max`
|
|
462
|
+
* characters of text from `props.text` / `props.value` fields. Used as a
|
|
463
|
+
* fallback excerpt when no `meta_description` is set.
|
|
464
|
+
*/
|
|
465
|
+
function extractText(node, max) {
|
|
466
|
+
const acc = [];
|
|
467
|
+
const walk = (n) => {
|
|
468
|
+
if (acc.join(' ').length >= max)
|
|
469
|
+
return;
|
|
470
|
+
if (!n || typeof n !== 'object')
|
|
471
|
+
return;
|
|
472
|
+
const obj = n;
|
|
473
|
+
const props = obj['props'];
|
|
474
|
+
if (props) {
|
|
475
|
+
if (typeof props['text'] === 'string')
|
|
476
|
+
acc.push(props['text']);
|
|
477
|
+
if (typeof props['value'] === 'string')
|
|
478
|
+
acc.push(props['value']);
|
|
479
|
+
}
|
|
480
|
+
const children = obj['children'];
|
|
481
|
+
if (Array.isArray(children)) {
|
|
482
|
+
for (const child of children)
|
|
483
|
+
walk(child);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
walk(node);
|
|
487
|
+
if (acc.length === 0)
|
|
488
|
+
return null;
|
|
489
|
+
const s = acc.join(' ').trim();
|
|
490
|
+
return s.length > max ? `${s.slice(0, max).trimEnd()}…` : s;
|
|
491
|
+
}
|
|
492
|
+
//# sourceMappingURL=blog-storefront-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-storefront-resolver.js","sourceRoot":"","sources":["../../../src/backend/services/blog-storefront-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type BlogTagDetail, type BlogTagInboundReferencesResponse, type CreateBlogTagRequest, type PatchBlogTagRequest } from '@endora-commerce/contracts';
|
|
3
|
+
import type { BlogCacheService } from './blog-cache.js';
|
|
4
|
+
/**
|
|
5
|
+
* BlogTagService — feature 016 / T065.
|
|
6
|
+
*
|
|
7
|
+
* CRUD over `blog_tags` with global `code` uniqueness (R14) and block-
|
|
8
|
+
* on-delete (FR-018): deletion is refused with 409 BLOG_TAG_IN_USE
|
|
9
|
+
* (listing the first 25 referencing posts) when at least one
|
|
10
|
+
* `blog_post_tags` row references the tag.
|
|
11
|
+
*
|
|
12
|
+
* Tag → Post attachment (`replaceForPost`) is owned by `BlogPostService`
|
|
13
|
+
* already (`setTags` keeps the ordered list per Post + bumps the post's
|
|
14
|
+
* version); this service exposes `getInboundReferences` to drive the
|
|
15
|
+
* admin's confirmation dialog before the operator commits a delete.
|
|
16
|
+
*/
|
|
17
|
+
export declare class BlogTagService {
|
|
18
|
+
private readonly emFactory;
|
|
19
|
+
private readonly cache?;
|
|
20
|
+
constructor(emFactory: () => EntityManager, cache?: BlogCacheService | undefined);
|
|
21
|
+
list(filters: {
|
|
22
|
+
q?: string | undefined;
|
|
23
|
+
page?: number | undefined;
|
|
24
|
+
perPage?: number | undefined;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
data: BlogTagDetail[];
|
|
27
|
+
pagination: {
|
|
28
|
+
page: number;
|
|
29
|
+
perPage: number;
|
|
30
|
+
totalPages: number;
|
|
31
|
+
totalItems: number;
|
|
32
|
+
};
|
|
33
|
+
}>;
|
|
34
|
+
getById(id: string): Promise<BlogTagDetail>;
|
|
35
|
+
create(input: CreateBlogTagRequest): Promise<BlogTagDetail>;
|
|
36
|
+
patch(id: string, input: PatchBlogTagRequest): Promise<BlogTagDetail>;
|
|
37
|
+
softDelete(id: string, version: number | undefined): Promise<void>;
|
|
38
|
+
getInboundReferences(id: string): Promise<BlogTagInboundReferencesResponse>;
|
|
39
|
+
private findRow;
|
|
40
|
+
private assertCodeAvailable;
|
|
41
|
+
private assertVersion;
|
|
42
|
+
private toDetail;
|
|
43
|
+
private toIso;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=blog-tag-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-tag-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/blog-tag-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,gCAAgC,EACrC,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAYxD;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;gBADN,SAAS,EAAE,MAAM,aAAa,EAC9B,KAAK,CAAC,EAAE,gBAAgB,YAAA;IAGrC,IAAI,CAAC,OAAO,EAAE;QAClB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KAC9B,GAAG,OAAO,CAAC;QACV,IAAI,EAAE,aAAa,EAAE,CAAC;QACtB,UAAU,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC;KACvF,CAAC;IAsCI,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAQ3C,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAyB3D,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,CAAC;IA0CrE,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA2ClE,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gCAAgC,CAAC;YA6BnE,OAAO;YAQP,mBAAmB;IAsBjC,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,KAAK;CAGd"}
|