@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,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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"}