@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,198 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
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 class BlogTagService {
|
|
18
|
+
emFactory;
|
|
19
|
+
cache;
|
|
20
|
+
constructor(emFactory, cache) {
|
|
21
|
+
this.emFactory = emFactory;
|
|
22
|
+
this.cache = cache;
|
|
23
|
+
}
|
|
24
|
+
async list(filters) {
|
|
25
|
+
const page = Math.max(1, filters.page ?? 1);
|
|
26
|
+
const perPage = Math.min(100, Math.max(1, filters.perPage ?? 50));
|
|
27
|
+
const em = this.emFactory();
|
|
28
|
+
const where = ['deleted_at is null'];
|
|
29
|
+
const params = [];
|
|
30
|
+
if (filters.q) {
|
|
31
|
+
where.push(`(name::text ilike ? or code ilike ?)`);
|
|
32
|
+
params.push(`%${filters.q}%`, `%${filters.q}%`);
|
|
33
|
+
}
|
|
34
|
+
const whereSql = `where ${where.join(' and ')}`;
|
|
35
|
+
const totalRows = (await em.execute(`select count(*)::int as n from blog_tags ${whereSql}`, params));
|
|
36
|
+
const totalItems = totalRows[0]?.n ?? 0;
|
|
37
|
+
const offset = (page - 1) * perPage;
|
|
38
|
+
const rows = (await em.execute(`select * from blog_tags ${whereSql}
|
|
39
|
+
order by code asc
|
|
40
|
+
limit ${perPage} offset ${offset}`, params));
|
|
41
|
+
return {
|
|
42
|
+
data: rows.map((r) => this.toDetail(r)),
|
|
43
|
+
pagination: {
|
|
44
|
+
page,
|
|
45
|
+
perPage,
|
|
46
|
+
totalPages: Math.max(1, Math.ceil(totalItems / perPage)),
|
|
47
|
+
totalItems,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async getById(id) {
|
|
52
|
+
const row = await this.findRow(this.emFactory(), id);
|
|
53
|
+
if (!row) {
|
|
54
|
+
throw new HttpError(404, ERROR_CODES.BLOG_TAG_NOT_FOUND, 'Blog tag not found.');
|
|
55
|
+
}
|
|
56
|
+
return this.toDetail(row);
|
|
57
|
+
}
|
|
58
|
+
async create(input) {
|
|
59
|
+
const em = this.emFactory();
|
|
60
|
+
const id = randomUUID();
|
|
61
|
+
const now = new Date();
|
|
62
|
+
return em.transactional(async (tx) => {
|
|
63
|
+
// Probe for friendly 409 before the partial-unique index fires.
|
|
64
|
+
await this.assertCodeAvailable(tx, input.code);
|
|
65
|
+
await tx.execute(`insert into blog_tags (id, name, description, code, version, created_at, updated_at)
|
|
66
|
+
values (?, ?::jsonb, ?::jsonb, ?, 1, ?, ?)`, [
|
|
67
|
+
id,
|
|
68
|
+
JSON.stringify(input.name),
|
|
69
|
+
input.description ? JSON.stringify(input.description) : null,
|
|
70
|
+
input.code,
|
|
71
|
+
now,
|
|
72
|
+
now,
|
|
73
|
+
]);
|
|
74
|
+
const row = await this.findRow(tx, id);
|
|
75
|
+
return this.toDetail(row);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async patch(id, input) {
|
|
79
|
+
const em = this.emFactory();
|
|
80
|
+
return em.transactional(async (tx) => {
|
|
81
|
+
const existing = await this.findRow(tx, id);
|
|
82
|
+
if (!existing) {
|
|
83
|
+
throw new HttpError(404, ERROR_CODES.BLOG_TAG_NOT_FOUND, 'Blog tag not found.');
|
|
84
|
+
}
|
|
85
|
+
this.assertVersion(existing.version, input.version);
|
|
86
|
+
const codeChange = input.code !== undefined && input.code !== existing.code;
|
|
87
|
+
if (codeChange) {
|
|
88
|
+
await this.assertCodeAvailable(tx, input.code, id);
|
|
89
|
+
}
|
|
90
|
+
const sets = ['version = version + 1', 'updated_at = now()'];
|
|
91
|
+
const params = [];
|
|
92
|
+
if (input.name !== undefined) {
|
|
93
|
+
sets.push('name = ?::jsonb');
|
|
94
|
+
params.push(JSON.stringify(input.name));
|
|
95
|
+
}
|
|
96
|
+
if (input.description !== undefined) {
|
|
97
|
+
sets.push('description = ?::jsonb');
|
|
98
|
+
params.push(input.description ? JSON.stringify(input.description) : null);
|
|
99
|
+
}
|
|
100
|
+
if (input.code !== undefined) {
|
|
101
|
+
sets.push('code = ?');
|
|
102
|
+
params.push(input.code);
|
|
103
|
+
}
|
|
104
|
+
params.push(id);
|
|
105
|
+
await tx.execute(`update blog_tags set ${sets.join(', ')} where id = ?`, params);
|
|
106
|
+
const row = await this.findRow(tx, id);
|
|
107
|
+
const detail = this.toDetail(row);
|
|
108
|
+
// Tag rename ⇒ blow the storefront cache (URLs change).
|
|
109
|
+
if (codeChange && this.cache)
|
|
110
|
+
await this.cache.invalidateAll();
|
|
111
|
+
return detail;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async softDelete(id, version) {
|
|
115
|
+
const em = this.emFactory();
|
|
116
|
+
return em.transactional(async (tx) => {
|
|
117
|
+
const existing = await this.findRow(tx, id);
|
|
118
|
+
if (!existing) {
|
|
119
|
+
throw new HttpError(404, ERROR_CODES.BLOG_TAG_NOT_FOUND, 'Blog tag not found.');
|
|
120
|
+
}
|
|
121
|
+
this.assertVersion(existing.version, version);
|
|
122
|
+
const refRows = (await tx.execute(`select count(*)::int as n from blog_post_tags where blog_tag_id = ?`, [id]));
|
|
123
|
+
if ((refRows[0]?.n ?? 0) > 0) {
|
|
124
|
+
// Build a useful details list (first 25 referencing posts).
|
|
125
|
+
const posts = (await tx.execute(`select p.id::text as id, p.name, p.slug
|
|
126
|
+
from blog_post_tags pt
|
|
127
|
+
join blog_posts p on p.id = pt.blog_post_id and p.deleted_at is null
|
|
128
|
+
where pt.blog_tag_id = ?
|
|
129
|
+
order by p.slug
|
|
130
|
+
limit 25`, [id]));
|
|
131
|
+
throw new HttpError(409, ERROR_CODES.BLOG_TAG_IN_USE, 'Tag is attached to one or more posts; detach it before deletion.', posts.map((p) => ({
|
|
132
|
+
path: 'tagId',
|
|
133
|
+
issue: `attached to post id=${p.id} slug=${p.slug}`,
|
|
134
|
+
})));
|
|
135
|
+
}
|
|
136
|
+
await tx.execute(`update blog_tags set deleted_at = now(), updated_at = now() where id = ?`, [id]);
|
|
137
|
+
if (this.cache)
|
|
138
|
+
await this.cache.invalidateAll();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async getInboundReferences(id) {
|
|
142
|
+
const em = this.emFactory();
|
|
143
|
+
const totalRows = (await em.execute(`select count(*)::int as n from blog_post_tags where blog_tag_id = ?`, [id]));
|
|
144
|
+
const totalPosts = totalRows[0]?.n ?? 0;
|
|
145
|
+
const rows = (await em.execute(`select p.id::text as id, p.name, p.slug
|
|
146
|
+
from blog_post_tags pt
|
|
147
|
+
join blog_posts p on p.id = pt.blog_post_id and p.deleted_at is null
|
|
148
|
+
where pt.blog_tag_id = ?
|
|
149
|
+
order by p.slug
|
|
150
|
+
limit 25`, [id]));
|
|
151
|
+
const posts = rows.map((r) => ({
|
|
152
|
+
id: r.id,
|
|
153
|
+
name: r.name,
|
|
154
|
+
slug: r.slug,
|
|
155
|
+
}));
|
|
156
|
+
return { posts, totalPosts };
|
|
157
|
+
}
|
|
158
|
+
// ────────────────────────────────────────────────────────────────────
|
|
159
|
+
// Internals
|
|
160
|
+
// ────────────────────────────────────────────────────────────────────
|
|
161
|
+
async findRow(em, id) {
|
|
162
|
+
const rows = (await em.execute(`select * from blog_tags where id = ? and deleted_at is null limit 1`, [id]));
|
|
163
|
+
return rows[0] ?? null;
|
|
164
|
+
}
|
|
165
|
+
async assertCodeAvailable(em, code, excludeId) {
|
|
166
|
+
const args = [code];
|
|
167
|
+
let sql = `select 1 from blog_tags where code = ? and deleted_at is null`;
|
|
168
|
+
if (excludeId) {
|
|
169
|
+
sql += ` and id <> ?`;
|
|
170
|
+
args.push(excludeId);
|
|
171
|
+
}
|
|
172
|
+
sql += ` limit 1`;
|
|
173
|
+
const rows = (await em.execute(sql, args));
|
|
174
|
+
if (rows.length > 0) {
|
|
175
|
+
throw new HttpError(409, ERROR_CODES.BLOG_TAG_CODE_TAKEN, `A blog tag with code "${code}" already exists.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
assertVersion(have, sent) {
|
|
179
|
+
if (sent !== undefined && sent !== have) {
|
|
180
|
+
throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, `Blog tag was updated concurrently (have v${have}, request v${sent}).`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
toDetail(row) {
|
|
184
|
+
return {
|
|
185
|
+
id: row.id,
|
|
186
|
+
name: row.name,
|
|
187
|
+
description: row.description,
|
|
188
|
+
code: row.code,
|
|
189
|
+
version: row.version,
|
|
190
|
+
createdAt: this.toIso(row.created_at),
|
|
191
|
+
updatedAt: this.toIso(row.updated_at),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
toIso(value) {
|
|
195
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=blog-tag-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-tag-service.js","sourceRoot":"","sources":["../../../src/backend/services/blog-tag-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
/**
|
|
3
|
+
* SeedDefaultCategory — feature 016 / R11.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent boot reconciler. On first run, creates a single
|
|
6
|
+
* `is_system = true` Category named "Default" (slug: 'default') and
|
|
7
|
+
* attaches it to every existing Sales Channel. On subsequent runs, when
|
|
8
|
+
* a system row already exists, the reconciler **does nothing** — admin
|
|
9
|
+
* edits to name / slug / scope / description / image / metadata are
|
|
10
|
+
* preserved (FR-004).
|
|
11
|
+
*
|
|
12
|
+
* Newly added Sales Channels do NOT get auto-attached to `Default` —
|
|
13
|
+
* the admin attaches them explicitly through the Categories editor. This
|
|
14
|
+
* matches the spec's R11 decision and avoids re-attaching a channel an
|
|
15
|
+
* admin has intentionally detached.
|
|
16
|
+
*
|
|
17
|
+
* The reconciler runs all reads + writes through the caller's single
|
|
18
|
+
* EntityManager so its behaviour is consistent across MikroORM transaction
|
|
19
|
+
* boundaries (tests + production share the same path).
|
|
20
|
+
*/
|
|
21
|
+
export declare function seedDefaultCategory(emFactory: () => EntityManager): Promise<{
|
|
22
|
+
categoryId: string;
|
|
23
|
+
created: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
//# sourceMappingURL=seed-default-category.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-default-category.d.ts","sourceRoot":"","sources":["../../../src/backend/services/seed-default-category.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,aAAa,GAC7B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAsCnD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
/**
|
|
4
|
+
* SeedDefaultCategory — feature 016 / R11.
|
|
5
|
+
*
|
|
6
|
+
* Idempotent boot reconciler. On first run, creates a single
|
|
7
|
+
* `is_system = true` Category named "Default" (slug: 'default') and
|
|
8
|
+
* attaches it to every existing Sales Channel. On subsequent runs, when
|
|
9
|
+
* a system row already exists, the reconciler **does nothing** — admin
|
|
10
|
+
* edits to name / slug / scope / description / image / metadata are
|
|
11
|
+
* preserved (FR-004).
|
|
12
|
+
*
|
|
13
|
+
* Newly added Sales Channels do NOT get auto-attached to `Default` —
|
|
14
|
+
* the admin attaches them explicitly through the Categories editor. This
|
|
15
|
+
* matches the spec's R11 decision and avoids re-attaching a channel an
|
|
16
|
+
* admin has intentionally detached.
|
|
17
|
+
*
|
|
18
|
+
* The reconciler runs all reads + writes through the caller's single
|
|
19
|
+
* EntityManager so its behaviour is consistent across MikroORM transaction
|
|
20
|
+
* boundaries (tests + production share the same path).
|
|
21
|
+
*/
|
|
22
|
+
export async function seedDefaultCategory(emFactory) {
|
|
23
|
+
const em = emFactory();
|
|
24
|
+
const existingRows = (await em.execute('select id::text as id from blog_categories where is_system = true and deleted_at is null limit 1'));
|
|
25
|
+
if (existingRows.length > 0) {
|
|
26
|
+
return { categoryId: existingRows[0].id, created: false };
|
|
27
|
+
}
|
|
28
|
+
const categoryId = randomUUID();
|
|
29
|
+
await em.execute(`insert into blog_categories
|
|
30
|
+
(id, slug, name, enabled, is_system, position, version, created_at, updated_at)
|
|
31
|
+
values
|
|
32
|
+
(?, 'default', '{"en-US":"Default"}'::jsonb, true, true, 0, 1, now(), now())`, [categoryId]);
|
|
33
|
+
/**
|
|
34
|
+
* The kernel's own entity, not `select id from sales_channels` (feature 075,
|
|
35
|
+
* D-87). `sales_channels` is the kernel's table since feature 072 moved the
|
|
36
|
+
* resolution machinery there, and a module relating into the kernel by ORM is
|
|
37
|
+
* the sanctioned access. It reads through the same `em` as the inserts below,
|
|
38
|
+
* so the enumeration and the bindings still share one connection.
|
|
39
|
+
*/
|
|
40
|
+
const channels = await em.find(SalesChannel, {}, { fields: ['id'] });
|
|
41
|
+
for (const channel of channels) {
|
|
42
|
+
await em.execute(`insert into blog_category_sales_channels
|
|
43
|
+
(blog_category_id, sales_channel_id, slug)
|
|
44
|
+
values (?, ?, 'default')`, [categoryId, channel.id]);
|
|
45
|
+
}
|
|
46
|
+
return { categoryId, created: true };
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=seed-default-category.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-default-category.js","sourceRoot":"","sources":["../../../src/backend/services/seed-default-category.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,SAA8B;IAE9B,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IAEvB,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CACpC,kGAAkG,CACnG,CAA0B,CAAC;IAC5B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAAE,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC7D,CAAC;IAED,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;IAChC,MAAM,EAAE,CAAC,OAAO,CACd;;;oFAGgF,EAChF,CAAC,UAAU,CAAC,CACb,CAAC;IAEF;;;;;;OAMG;IACH,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAErE,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,EAAE,CAAC,OAAO,CACd;;kCAE4B,EAC5B,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,CACzB,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AdminRolePort, SystemRoleCodePort } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* SeedBlogRoles — feature 016 / R8 / T025.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent boot reconciler. Upserts two seeded admin roles:
|
|
6
|
+
*
|
|
7
|
+
* blog_manager ['blog.read', 'blog.write']
|
|
8
|
+
* content_manager ['blog.read', 'blog.write', 'cms.read', 'cms.write']
|
|
9
|
+
*
|
|
10
|
+
* On rerun, the reconciler keeps the admin-edited `name` field intact
|
|
11
|
+
* (FR-025 idempotency) and only refreshes the canonical permission array
|
|
12
|
+
* if it has drifted from the documented set. This mirrors feature 014's
|
|
13
|
+
* Hooks-seeder pattern.
|
|
14
|
+
*
|
|
15
|
+
* **Both rows are written through `adminRolePort`** since feature 075 drained
|
|
16
|
+
* `blog`'s cross-module-import shard. Until then this file selected, inserted
|
|
17
|
+
* and updated the `admin_roles` table in raw SQL — a boundary that compiled and
|
|
18
|
+
* returned rows, because a statement in a string names no import specifier, and
|
|
19
|
+
* the one entry the shard carried. Two things follow from the port that the SQL
|
|
20
|
+
* did not give us: the seeded rows now carry an audit entry like every other
|
|
21
|
+
* role write (Constitution XIII), and their permission codes are validated
|
|
22
|
+
* against the platform's catalogue rather than trusted.
|
|
23
|
+
*
|
|
24
|
+
* `upsertByCode` assigns `name` from its input unconditionally, so preserving
|
|
25
|
+
* an operator's rename is this seeder's job and is done the only way the port's
|
|
26
|
+
* shape allows: read the row first, and hand its current name straight back on
|
|
27
|
+
* the refresh. A blind upsert here would silently rename "Bloger" to
|
|
28
|
+
* "Blog Manager" at every boot.
|
|
29
|
+
*/
|
|
30
|
+
export declare const BLOG_ROLE_CODES: {
|
|
31
|
+
readonly BLOG_MANAGER: "blog_manager";
|
|
32
|
+
readonly CONTENT_MANAGER: "content_manager";
|
|
33
|
+
};
|
|
34
|
+
export interface BlogRoleSeedResult {
|
|
35
|
+
code: string;
|
|
36
|
+
id: string;
|
|
37
|
+
created: boolean;
|
|
38
|
+
permissionsRefreshed: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function seedBlogRoles(adminRoles: AdminRolePort, systemRoleCodes: SystemRoleCodePort): Promise<BlogRoleSeedResult[]>;
|
|
41
|
+
//# sourceMappingURL=seed-roles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-roles.d.ts","sourceRoot":"","sources":["../../../src/backend/services/seed-roles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,eAAO,MAAM,eAAe;;;CAGlB,CAAC;AAmBX,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAQD,wBAAsB,aAAa,CACjC,UAAU,EAAE,aAAa,EACzB,eAAe,EAAE,kBAAkB,GAClC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAoD/B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeedBlogRoles — feature 016 / R8 / T025.
|
|
3
|
+
*
|
|
4
|
+
* Idempotent boot reconciler. Upserts two seeded admin roles:
|
|
5
|
+
*
|
|
6
|
+
* blog_manager ['blog.read', 'blog.write']
|
|
7
|
+
* content_manager ['blog.read', 'blog.write', 'cms.read', 'cms.write']
|
|
8
|
+
*
|
|
9
|
+
* On rerun, the reconciler keeps the admin-edited `name` field intact
|
|
10
|
+
* (FR-025 idempotency) and only refreshes the canonical permission array
|
|
11
|
+
* if it has drifted from the documented set. This mirrors feature 014's
|
|
12
|
+
* Hooks-seeder pattern.
|
|
13
|
+
*
|
|
14
|
+
* **Both rows are written through `adminRolePort`** since feature 075 drained
|
|
15
|
+
* `blog`'s cross-module-import shard. Until then this file selected, inserted
|
|
16
|
+
* and updated the `admin_roles` table in raw SQL — a boundary that compiled and
|
|
17
|
+
* returned rows, because a statement in a string names no import specifier, and
|
|
18
|
+
* the one entry the shard carried. Two things follow from the port that the SQL
|
|
19
|
+
* did not give us: the seeded rows now carry an audit entry like every other
|
|
20
|
+
* role write (Constitution XIII), and their permission codes are validated
|
|
21
|
+
* against the platform's catalogue rather than trusted.
|
|
22
|
+
*
|
|
23
|
+
* `upsertByCode` assigns `name` from its input unconditionally, so preserving
|
|
24
|
+
* an operator's rename is this seeder's job and is done the only way the port's
|
|
25
|
+
* shape allows: read the row first, and hand its current name straight back on
|
|
26
|
+
* the refresh. A blind upsert here would silently rename "Bloger" to
|
|
27
|
+
* "Blog Manager" at every boot.
|
|
28
|
+
*/
|
|
29
|
+
export const BLOG_ROLE_CODES = {
|
|
30
|
+
BLOG_MANAGER: 'blog_manager',
|
|
31
|
+
CONTENT_MANAGER: 'content_manager',
|
|
32
|
+
};
|
|
33
|
+
const SEED_DEFINITIONS = [
|
|
34
|
+
{
|
|
35
|
+
code: BLOG_ROLE_CODES.BLOG_MANAGER,
|
|
36
|
+
defaultName: 'Blog Manager',
|
|
37
|
+
permissions: ['blog.read', 'blog.write'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
code: BLOG_ROLE_CODES.CONTENT_MANAGER,
|
|
41
|
+
defaultName: 'Content Manager',
|
|
42
|
+
permissions: ['blog.read', 'blog.write', 'cms.read', 'cms.write'],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
function hasDrifted(want, have) {
|
|
46
|
+
const wanted = [...want].sort();
|
|
47
|
+
const held = [...have].sort();
|
|
48
|
+
return wanted.length !== held.length || wanted.some((code, i) => code !== held[i]);
|
|
49
|
+
}
|
|
50
|
+
export async function seedBlogRoles(adminRoles, systemRoleCodes) {
|
|
51
|
+
// Register the seeded codes as system-protected up-front. Idempotent,
|
|
52
|
+
// and the registration must happen even when the rows already exist
|
|
53
|
+
// (a fresh process boot starts with an empty in-memory registry).
|
|
54
|
+
//
|
|
55
|
+
// Through `systemRoleCodePort` since feature 075's Phase C, where this used
|
|
56
|
+
// to import `admin_roles`' module-level `registerSystemRoleCode`. The seam is
|
|
57
|
+
// the same one and its classification is unchanged: a contribution into an
|
|
58
|
+
// ungated registry, so a `blog` that is off registers nothing and its seeded
|
|
59
|
+
// roles are simply not protected — which is the right answer, because a
|
|
60
|
+
// module that is not there has no seeded role to protect.
|
|
61
|
+
for (const def of SEED_DEFINITIONS) {
|
|
62
|
+
systemRoleCodes.register(def.code);
|
|
63
|
+
}
|
|
64
|
+
const results = [];
|
|
65
|
+
for (const def of SEED_DEFINITIONS) {
|
|
66
|
+
const existing = await adminRoles.findByCode(def.code);
|
|
67
|
+
if (existing === null) {
|
|
68
|
+
const created = await adminRoles.upsertByCode({
|
|
69
|
+
code: def.code,
|
|
70
|
+
name: def.defaultName,
|
|
71
|
+
permissions: def.permissions,
|
|
72
|
+
requiresTwoFactor: false,
|
|
73
|
+
});
|
|
74
|
+
results.push({ code: def.code, id: created.id, created: true, permissionsRefreshed: false });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const drifted = hasDrifted(def.permissions, existing.permissions);
|
|
78
|
+
if (drifted) {
|
|
79
|
+
await adminRoles.upsertByCode({
|
|
80
|
+
code: def.code,
|
|
81
|
+
// The operator's own name and two-factor choice, handed back
|
|
82
|
+
// unchanged: this call exists to correct the permission array and
|
|
83
|
+
// nothing else.
|
|
84
|
+
name: existing.name,
|
|
85
|
+
permissions: def.permissions,
|
|
86
|
+
requiresTwoFactor: existing.requiresTwoFactor,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
results.push({
|
|
90
|
+
code: def.code,
|
|
91
|
+
id: existing.id,
|
|
92
|
+
created: false,
|
|
93
|
+
permissionsRefreshed: drifted,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=seed-roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-roles.js","sourceRoot":"","sources":["../../../src/backend/services/seed-roles.ts"],"names":[],"mappings":"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"}
|