@carlonicora/nextjs-jsonapi 5.3.0 → 5.4.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/dist/{BlockNoteEditor-UJ6363LC.js → BlockNoteEditor-4YCAIEPT.js} +23 -22
- package/dist/BlockNoteEditor-4YCAIEPT.js.map +1 -0
- package/dist/{BlockNoteEditor-WTSQANKI.mjs → BlockNoteEditor-YQL2SGFR.mjs} +8 -7
- package/dist/BlockNoteEditor-YQL2SGFR.mjs.map +1 -0
- package/dist/{BlockNoteViewer-JVKWJZ6J.mjs → BlockNoteViewer-AEJKJ27Q.mjs} +2 -2
- package/dist/{BlockNoteViewer-EL4JFB6P.js → BlockNoteViewer-YL3J7J2Y.js} +3 -3
- package/dist/{BlockNoteViewer-EL4JFB6P.js.map → BlockNoteViewer-YL3J7J2Y.js.map} +1 -1
- package/dist/BlogCategoryFields-DO48vOTE.d.mts +14 -0
- package/dist/BlogCategoryFields-DO48vOTE.d.ts +14 -0
- package/dist/BlogPostInterface-BB_4wcDR.d.mts +124 -0
- package/dist/BlogPostInterface-bWAT1rfR.d.ts +124 -0
- package/dist/billing/index.js +441 -441
- package/dist/billing/index.mjs +3 -3
- package/dist/blog-public.types-BbrzsC1c.d.mts +30 -0
- package/dist/blog-public.types-BbrzsC1c.d.ts +30 -0
- package/dist/{chunk-3BAM23VA.js → chunk-3NLAXVIN.js} +860 -849
- package/dist/chunk-3NLAXVIN.js.map +1 -0
- package/dist/{chunk-QKZUE5OF.mjs → chunk-3YW35YHQ.mjs} +1 -101
- package/dist/chunk-3YW35YHQ.mjs.map +1 -0
- package/dist/{chunk-QQQE3N2X.js → chunk-5PPG6PL7.js} +3 -3
- package/dist/{chunk-QQQE3N2X.js.map → chunk-5PPG6PL7.js.map} +1 -1
- package/dist/{chunk-FNTQQFLH.mjs → chunk-6DC2WVVB.mjs} +2 -2
- package/dist/{chunk-MIZTWBPX.mjs → chunk-EUKTBOI2.mjs} +19 -8
- package/dist/chunk-EUKTBOI2.mjs.map +1 -0
- package/dist/{chunk-XGTQWBDJ.js → chunk-KSYQEKJX.js} +11 -111
- package/dist/chunk-KSYQEKJX.js.map +1 -0
- package/dist/{chunk-CRI4T47Y.js → chunk-NFBOSPNJ.js} +756 -41
- package/dist/chunk-NFBOSPNJ.js.map +1 -0
- package/dist/{chunk-6WU74VFU.mjs → chunk-QMYQRPQC.mjs} +2 -2
- package/dist/{chunk-JQWI62NV.mjs → chunk-WC63PJSC.mjs} +716 -1
- package/dist/chunk-WC63PJSC.mjs.map +1 -0
- package/dist/chunk-XCHSX3VC.mjs +107 -0
- package/dist/chunk-XCHSX3VC.mjs.map +1 -0
- package/dist/{chunk-EO75CV76.js → chunk-YNCQSWQW.js} +8 -8
- package/dist/{chunk-EO75CV76.js.map → chunk-YNCQSWQW.js.map} +1 -1
- package/dist/chunk-YW4ZCEWB.js +107 -0
- package/dist/chunk-YW4ZCEWB.js.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +13 -2
- package/dist/components/index.d.ts +13 -2
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +214 -3
- package/dist/core/index.d.ts +214 -3
- package/dist/core/index.js +36 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +35 -1
- package/dist/email-automations/index.js +92 -92
- package/dist/email-automations/index.mjs +3 -3
- package/dist/features/analytics/index.js +122 -122
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +6 -5
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +4 -3
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.css +30 -0
- package/dist/features/blog/index.css.map +1 -0
- package/dist/features/blog/index.d.mts +266 -0
- package/dist/features/blog/index.d.ts +266 -0
- package/dist/features/blog/index.js +1799 -0
- package/dist/features/blog/index.js.map +1 -0
- package/dist/features/blog/index.mjs +1799 -0
- package/dist/features/blog/index.mjs.map +1 -0
- package/dist/features/blog/server.d.mts +33 -0
- package/dist/features/blog/server.d.ts +33 -0
- package/dist/features/blog/server.js +139 -0
- package/dist/features/blog/server.js.map +1 -0
- package/dist/features/blog/server.mjs +139 -0
- package/dist/features/blog/server.mjs.map +1 -0
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +125 -125
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +37 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -2
- package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
- package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +8 -6
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +12 -10
- package/messages/en.json +93 -0
- package/messages/it.json +93 -0
- package/package.json +11 -1
- package/src/components/editors/BlockNoteEditor.tsx +13 -3
- package/src/components/forms/FormBlockNote.tsx +4 -0
- package/src/components/navigations/ContentTitle.tsx +14 -3
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +6 -0
- package/src/features/blog/BlogModules.ts +49 -0
- package/src/features/blog/components/admin/BlogAdminContainer.tsx +32 -0
- package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
- package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
- package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
- package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
- package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
- package/src/features/blog/components/admin/BlogPostContainer.tsx +102 -0
- package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
- package/src/features/blog/components/admin/BlogPostEditor.tsx +207 -0
- package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
- package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +110 -0
- package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
- package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
- package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
- package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
- package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
- package/src/features/blog/components/admin/index.ts +14 -0
- package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
- package/src/features/blog/components/public/BlogList.tsx +21 -0
- package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
- package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
- package/src/features/blog/components/public/BlogListParts.tsx +74 -0
- package/src/features/blog/components/public/BlogListRail.tsx +62 -0
- package/src/features/blog/components/public/BlogPostCard.tsx +132 -0
- package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
- package/src/features/blog/components/public/BlogPostView.tsx +74 -0
- package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
- package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +173 -0
- package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +128 -0
- package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
- package/src/features/blog/components/public/index.ts +3 -0
- package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
- package/src/features/blog/contexts/index.ts +1 -0
- package/src/features/blog/data/BlogCategory.ts +93 -0
- package/src/features/blog/data/BlogCategoryFields.ts +4 -0
- package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
- package/src/features/blog/data/BlogCategoryService.ts +54 -0
- package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
- package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
- package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
- package/src/features/blog/data/BlogPost.spec.ts +123 -0
- package/src/features/blog/data/BlogPost.ts +159 -0
- package/src/features/blog/data/BlogPostFields.ts +7 -0
- package/src/features/blog/data/BlogPostInterface.ts +39 -0
- package/src/features/blog/data/BlogPostService.ts +82 -0
- package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
- package/src/features/blog/data/BlogPostTranslation.ts +110 -0
- package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
- package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
- package/src/features/blog/data/BlogSettings.ts +37 -0
- package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
- package/src/features/blog/data/BlogSettingsService.ts +24 -0
- package/src/features/blog/data/index.ts +17 -0
- package/src/features/blog/hooks/index.ts +3 -0
- package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
- package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
- package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
- package/src/features/blog/index.ts +15 -0
- package/src/features/blog/server/fetchers.ts +120 -0
- package/src/features/blog/server/toPublic.spec.ts +91 -0
- package/src/features/blog/server/toPublic.ts +36 -0
- package/src/features/blog/server-entry.ts +17 -0
- package/src/features/blog/types/blog-public.types.ts +34 -0
- package/src/features/blog/types/blog.constants.ts +40 -0
- package/src/features/s3/data/s3.service.ts +17 -0
- package/dist/BlockNoteEditor-UJ6363LC.js.map +0 -1
- package/dist/BlockNoteEditor-WTSQANKI.mjs.map +0 -1
- package/dist/chunk-3BAM23VA.js.map +0 -1
- package/dist/chunk-CRI4T47Y.js.map +0 -1
- package/dist/chunk-JQWI62NV.mjs.map +0 -1
- package/dist/chunk-MIZTWBPX.mjs.map +0 -1
- package/dist/chunk-QKZUE5OF.mjs.map +0 -1
- package/dist/chunk-XGTQWBDJ.js.map +0 -1
- /package/dist/{BlockNoteViewer-JVKWJZ6J.mjs.map → BlockNoteViewer-AEJKJ27Q.mjs.map} +0 -0
- /package/dist/{chunk-FNTQQFLH.mjs.map → chunk-6DC2WVVB.mjs.map} +0 -0
- /package/dist/{chunk-6WU74VFU.mjs.map → chunk-QMYQRPQC.mjs.map} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
import { BlogAiStatus, BlogTranslationSource } from "../types/blog.constants";
|
|
3
|
+
import { BlogCategoryInterface } from "./BlogCategoryInterface";
|
|
4
|
+
|
|
5
|
+
export type BlogCategoryTranslationInput = {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
slug?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface BlogCategoryTranslationInterface extends ApiDataInterface {
|
|
13
|
+
get language(): string;
|
|
14
|
+
get isOriginal(): boolean;
|
|
15
|
+
get source(): BlogTranslationSource;
|
|
16
|
+
get aiStatus(): BlogAiStatus;
|
|
17
|
+
get name(): string;
|
|
18
|
+
get slug(): string;
|
|
19
|
+
get description(): string | undefined;
|
|
20
|
+
get translatedAt(): Date | undefined;
|
|
21
|
+
get category(): BlogCategoryInterface | undefined;
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
|
|
2
|
+
import { BLOG_ACTION } from "../types/blog.constants";
|
|
3
|
+
import { BlogCategoryTranslationInput, BlogCategoryTranslationInterface } from "./BlogCategoryTranslationInterface";
|
|
4
|
+
|
|
5
|
+
export class BlogCategoryTranslationService extends AbstractService {
|
|
6
|
+
static async update(params: BlogCategoryTranslationInput): Promise<BlogCategoryTranslationInterface> {
|
|
7
|
+
return this.callApi<BlogCategoryTranslationInterface>({
|
|
8
|
+
type: Modules.BlogCategoryTranslation,
|
|
9
|
+
method: HttpMethod.PUT,
|
|
10
|
+
endpoint: new EndpointCreator({ endpoint: Modules.BlogCategoryTranslation, id: params.id }).generate(),
|
|
11
|
+
input: params,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* POST blog-category-translations/:id/retranslate. Re-queues this one
|
|
17
|
+
* language from the current original. Replies 204.
|
|
18
|
+
*/
|
|
19
|
+
static async retranslate(params: { id: string }): Promise<void> {
|
|
20
|
+
await this.callApi({
|
|
21
|
+
type: Modules.BlogCategoryTranslation,
|
|
22
|
+
method: HttpMethod.POST,
|
|
23
|
+
endpoint: new EndpointCreator({
|
|
24
|
+
endpoint: Modules.BlogCategoryTranslation,
|
|
25
|
+
id: params.id,
|
|
26
|
+
childEndpoint: BLOG_ACTION.retranslate,
|
|
27
|
+
}).generate(),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { User } from "../../user/data/user";
|
|
3
|
+
import { ModuleRegistry } from "../../../core/registry/ModuleRegistry";
|
|
4
|
+
import { ModuleFactory, ModuleWithPermissions } from "../../../permissions/types";
|
|
5
|
+
import {
|
|
6
|
+
BlogCategoryModule,
|
|
7
|
+
BlogCategoryTranslationModule,
|
|
8
|
+
BlogPostModule,
|
|
9
|
+
BlogPostTranslationModule,
|
|
10
|
+
BlogSettingsModule,
|
|
11
|
+
} from "../BlogModules";
|
|
12
|
+
import { BlogPost } from "./BlogPost";
|
|
13
|
+
import { BlogPostTranslation } from "./BlogPostTranslation";
|
|
14
|
+
import { BlogCategory } from "./BlogCategory";
|
|
15
|
+
import { DataClassRegistry } from "../../../core/registry/DataClassRegistry";
|
|
16
|
+
import { Modules } from "../../../core/registry/ModuleRegistry";
|
|
17
|
+
|
|
18
|
+
// createJsonApi reads Modules.X.name (resolved lazily via ModuleRegistry), so
|
|
19
|
+
// the modules must be registered before a model serialises — mirrors the
|
|
20
|
+
// email-automation-models.spec setup convention in this package.
|
|
21
|
+
const moduleFactory: ModuleFactory = (params) =>
|
|
22
|
+
({
|
|
23
|
+
pageUrl: params.pageUrl,
|
|
24
|
+
name: params.name,
|
|
25
|
+
model: params.model,
|
|
26
|
+
}) as ModuleWithPermissions;
|
|
27
|
+
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
ModuleRegistry.register("BlogPost", BlogPostModule(moduleFactory));
|
|
30
|
+
ModuleRegistry.register("BlogPostTranslation", BlogPostTranslationModule(moduleFactory));
|
|
31
|
+
ModuleRegistry.register("BlogCategory", BlogCategoryModule(moduleFactory));
|
|
32
|
+
ModuleRegistry.register("BlogCategoryTranslation", BlogCategoryTranslationModule(moduleFactory));
|
|
33
|
+
ModuleRegistry.register("BlogSettings", BlogSettingsModule(moduleFactory));
|
|
34
|
+
ModuleRegistry.register("User", { name: "users", model: User } as ModuleWithPermissions);
|
|
35
|
+
DataClassRegistry.registerObjectClass(Modules.BlogPostTranslation, BlogPostTranslation);
|
|
36
|
+
DataClassRegistry.registerObjectClass(Modules.BlogCategory, BlogCategory);
|
|
37
|
+
DataClassRegistry.registerObjectClass(Modules.User, User);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const blocks = [{ type: "paragraph", content: [{ type: "text", text: "Hello", styles: {} }] }];
|
|
41
|
+
|
|
42
|
+
describe("BlogPost", () => {
|
|
43
|
+
it("BlogPost.createJsonApi stringifies the body and sends categories", () => {
|
|
44
|
+
const p = new BlogPost().createJsonApi({
|
|
45
|
+
id: "p",
|
|
46
|
+
originalLanguage: "en",
|
|
47
|
+
title: "T",
|
|
48
|
+
body: [{ type: "paragraph" }],
|
|
49
|
+
categoryIds: ["c1"],
|
|
50
|
+
});
|
|
51
|
+
expect(p.data.type).toBe("blog-posts");
|
|
52
|
+
expect(typeof p.data.attributes.body).toBe("string");
|
|
53
|
+
expect(JSON.parse(p.data.attributes.body)).toEqual([{ type: "paragraph" }]);
|
|
54
|
+
expect(p.data.relationships.categories.data).toEqual([{ type: "blog-categories", id: "c1" }]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("never sends server-owned attributes", () => {
|
|
58
|
+
const p = new BlogPost().createJsonApi({ id: "p", title: "T" });
|
|
59
|
+
expect(p.data.attributes).toEqual({ title: "T" });
|
|
60
|
+
expect("categories" in p.data.relationships).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("BlogPost.rehydrate parses publishedAt as a Date and the resolved body as blocks", () => {
|
|
64
|
+
const post = new BlogPost().rehydrate({
|
|
65
|
+
jsonApi: {
|
|
66
|
+
id: "p",
|
|
67
|
+
type: "blog-posts",
|
|
68
|
+
attributes: {
|
|
69
|
+
status: "published",
|
|
70
|
+
originalLanguage: "en",
|
|
71
|
+
publishedAt: "2026-10-12T09:00:00.000Z",
|
|
72
|
+
language: "it",
|
|
73
|
+
title: "Titolo",
|
|
74
|
+
slug: "titolo",
|
|
75
|
+
body: JSON.stringify(blocks),
|
|
76
|
+
readingMinutes: 3,
|
|
77
|
+
authorName: "Ada",
|
|
78
|
+
categoryIds: ["c1"],
|
|
79
|
+
},
|
|
80
|
+
relationships: {
|
|
81
|
+
translations: { data: [{ type: "blog-post-translations", id: "t1" }] },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
included: [
|
|
85
|
+
{
|
|
86
|
+
id: "t1",
|
|
87
|
+
type: "blog-post-translations",
|
|
88
|
+
attributes: { language: "en", isOriginal: true, source: "original", aiStatus: "completed", title: "Title" },
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
} as any);
|
|
92
|
+
|
|
93
|
+
expect(post.publishedAt).toBeInstanceOf(Date);
|
|
94
|
+
expect(post.publishedAt?.toISOString()).toBe("2026-10-12T09:00:00.000Z");
|
|
95
|
+
expect(Array.isArray(post.body)).toBe(true);
|
|
96
|
+
expect(post.body).toEqual(blocks);
|
|
97
|
+
expect(post.status).toBe("published");
|
|
98
|
+
expect(post.language).toBe("it");
|
|
99
|
+
expect(post.title).toBe("Titolo");
|
|
100
|
+
expect(post.readingMinutes).toBe(3);
|
|
101
|
+
expect(post.authorName).toBe("Ada");
|
|
102
|
+
expect(post.categoryIds).toEqual(["c1"]);
|
|
103
|
+
expect(post.translations).toHaveLength(1);
|
|
104
|
+
expect(post.original?.title).toBe("Title");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reads resolved values sent as meta", () => {
|
|
108
|
+
const post = new BlogPost().rehydrate({
|
|
109
|
+
jsonApi: {
|
|
110
|
+
id: "p",
|
|
111
|
+
type: "blog-posts",
|
|
112
|
+
attributes: { status: "draft", originalLanguage: "en" },
|
|
113
|
+
meta: { title: "From meta", categoryIds: ["c2"] },
|
|
114
|
+
},
|
|
115
|
+
included: [],
|
|
116
|
+
} as any);
|
|
117
|
+
|
|
118
|
+
expect(post.title).toBe("From meta");
|
|
119
|
+
expect(post.categoryIds).toEqual(["c2"]);
|
|
120
|
+
expect(post.publishedAt).toBeUndefined();
|
|
121
|
+
expect(post.body).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { UserInterface } from "../../user/data/user.interface";
|
|
3
|
+
import { BlogPostStatus } from "../types/blog.constants";
|
|
4
|
+
import { BlogCategoryInterface } from "./BlogCategoryInterface";
|
|
5
|
+
import { BlogPostInput, BlogPostInterface } from "./BlogPostInterface";
|
|
6
|
+
import { BlogPostTranslationInterface } from "./BlogPostTranslationInterface";
|
|
7
|
+
|
|
8
|
+
export class BlogPost extends AbstractApiData implements BlogPostInterface {
|
|
9
|
+
private _status?: BlogPostStatus;
|
|
10
|
+
private _originalLanguage?: string;
|
|
11
|
+
private _coverImage?: string;
|
|
12
|
+
private _publishedAt?: Date;
|
|
13
|
+
private _author?: UserInterface;
|
|
14
|
+
private _categories?: BlogCategoryInterface[];
|
|
15
|
+
private _translations?: BlogPostTranslationInterface[];
|
|
16
|
+
|
|
17
|
+
private _language?: string;
|
|
18
|
+
private _title?: string;
|
|
19
|
+
private _slug?: string;
|
|
20
|
+
private _summary?: string;
|
|
21
|
+
private _body?: any;
|
|
22
|
+
private _coverImageAlt?: string;
|
|
23
|
+
private _readingMinutes?: number;
|
|
24
|
+
private _authorName?: string;
|
|
25
|
+
private _categoryIds?: string[];
|
|
26
|
+
|
|
27
|
+
get status(): BlogPostStatus {
|
|
28
|
+
return this._status ?? "draft";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get originalLanguage(): string {
|
|
32
|
+
return this._originalLanguage ?? "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get coverImage(): string | undefined {
|
|
36
|
+
return this._coverImage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get publishedAt(): Date | undefined {
|
|
40
|
+
return this._publishedAt;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get author(): UserInterface | undefined {
|
|
44
|
+
return this._author;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get categories(): BlogCategoryInterface[] {
|
|
48
|
+
return this._categories ?? [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get translations(): BlogPostTranslationInterface[] {
|
|
52
|
+
return this._translations ?? [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get original(): BlogPostTranslationInterface | undefined {
|
|
56
|
+
return this.translations.find((translation) => translation.isOriginal);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get language(): string | undefined {
|
|
60
|
+
return this._language;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get title(): string | undefined {
|
|
64
|
+
return this._title;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get slug(): string | undefined {
|
|
68
|
+
return this._slug;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get summary(): string | undefined {
|
|
72
|
+
return this._summary;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get body(): any {
|
|
76
|
+
return this._body ?? [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get coverImageAlt(): string | undefined {
|
|
80
|
+
return this._coverImageAlt;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get readingMinutes(): number | undefined {
|
|
84
|
+
return this._readingMinutes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get authorName(): string | undefined {
|
|
88
|
+
return this._authorName;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get categoryIds(): string[] {
|
|
92
|
+
return this._categoryIds ?? [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
96
|
+
super.rehydrate(data);
|
|
97
|
+
|
|
98
|
+
const attributes = data.jsonApi.attributes ?? {};
|
|
99
|
+
const meta = data.jsonApi.meta ?? {};
|
|
100
|
+
// The resolved-language values are computed on the API. Depending on the
|
|
101
|
+
// descriptor they arrive as attributes or as meta, so both are read.
|
|
102
|
+
const resolved = (key: string) => attributes[key] ?? meta[key] ?? undefined;
|
|
103
|
+
|
|
104
|
+
this._status = attributes.status;
|
|
105
|
+
this._originalLanguage = attributes.originalLanguage;
|
|
106
|
+
this._coverImage = attributes.coverImage ?? undefined;
|
|
107
|
+
this._publishedAt = attributes.publishedAt ? new Date(attributes.publishedAt) : undefined;
|
|
108
|
+
|
|
109
|
+
this._language = resolved("language");
|
|
110
|
+
this._title = resolved("title");
|
|
111
|
+
this._slug = resolved("slug");
|
|
112
|
+
this._summary = resolved("summary");
|
|
113
|
+
const body = resolved("body");
|
|
114
|
+
this._body = body ? JSON.parse(body) : undefined;
|
|
115
|
+
this._coverImageAlt = resolved("coverImageAlt");
|
|
116
|
+
this._readingMinutes = resolved("readingMinutes");
|
|
117
|
+
this._authorName = resolved("authorName");
|
|
118
|
+
this._categoryIds = resolved("categoryIds") ?? [];
|
|
119
|
+
|
|
120
|
+
this._author = this._readIncluded(data, "author", Modules.User) as UserInterface;
|
|
121
|
+
this._categories = this._readIncluded(data, "categories", Modules.BlogCategory) as BlogCategoryInterface[];
|
|
122
|
+
this._translations = this._readIncluded(
|
|
123
|
+
data,
|
|
124
|
+
"translations",
|
|
125
|
+
Modules.BlogPostTranslation,
|
|
126
|
+
) as BlogPostTranslationInterface[];
|
|
127
|
+
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
createJsonApi(data: BlogPostInput) {
|
|
132
|
+
const response: any = {
|
|
133
|
+
data: {
|
|
134
|
+
type: Modules.BlogPost.name,
|
|
135
|
+
id: data.id,
|
|
136
|
+
attributes: {},
|
|
137
|
+
meta: {},
|
|
138
|
+
relationships: {},
|
|
139
|
+
},
|
|
140
|
+
included: [],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (data.originalLanguage !== undefined) response.data.attributes.originalLanguage = data.originalLanguage;
|
|
144
|
+
if (data.title !== undefined) response.data.attributes.title = data.title;
|
|
145
|
+
if (data.slug !== undefined) response.data.attributes.slug = data.slug;
|
|
146
|
+
if (data.summary !== undefined) response.data.attributes.summary = data.summary;
|
|
147
|
+
if (data.body !== undefined) response.data.attributes.body = JSON.stringify(data.body);
|
|
148
|
+
if (data.coverImage !== undefined) response.data.attributes.coverImage = data.coverImage;
|
|
149
|
+
if (data.coverImageAlt !== undefined) response.data.attributes.coverImageAlt = data.coverImageAlt;
|
|
150
|
+
|
|
151
|
+
if (data.categoryIds !== undefined) {
|
|
152
|
+
response.data.relationships.categories = {
|
|
153
|
+
data: data.categoryIds.map((id) => ({ type: Modules.BlogCategory.name, id })),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
import { UserInterface } from "../../user/data/user.interface";
|
|
3
|
+
import { BlogPostStatus } from "../types/blog.constants";
|
|
4
|
+
import { BlogCategoryInterface } from "./BlogCategoryInterface";
|
|
5
|
+
import { BlogPostTranslationInterface } from "./BlogPostTranslationInterface";
|
|
6
|
+
|
|
7
|
+
export type BlogPostInput = {
|
|
8
|
+
id: string;
|
|
9
|
+
originalLanguage?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
slug?: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
body?: any;
|
|
14
|
+
coverImage?: string;
|
|
15
|
+
coverImageAlt?: string;
|
|
16
|
+
categoryIds?: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface BlogPostInterface extends ApiDataInterface {
|
|
20
|
+
get status(): BlogPostStatus;
|
|
21
|
+
get originalLanguage(): string;
|
|
22
|
+
get coverImage(): string | undefined;
|
|
23
|
+
get publishedAt(): Date | undefined;
|
|
24
|
+
get author(): UserInterface | undefined;
|
|
25
|
+
get categories(): BlogCategoryInterface[];
|
|
26
|
+
get translations(): BlogPostTranslationInterface[];
|
|
27
|
+
get original(): BlogPostTranslationInterface | undefined;
|
|
28
|
+
|
|
29
|
+
// Resolved for one language by the public and list queries.
|
|
30
|
+
get language(): string | undefined;
|
|
31
|
+
get title(): string | undefined;
|
|
32
|
+
get slug(): string | undefined;
|
|
33
|
+
get summary(): string | undefined;
|
|
34
|
+
get body(): any;
|
|
35
|
+
get coverImageAlt(): string | undefined;
|
|
36
|
+
get readingMinutes(): number | undefined;
|
|
37
|
+
get authorName(): string | undefined;
|
|
38
|
+
get categoryIds(): string[];
|
|
39
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef } from "../../../core";
|
|
2
|
+
import { BLOG_ACTION } from "../types/blog.constants";
|
|
3
|
+
import { BlogPostInput, BlogPostInterface } from "./BlogPostInterface";
|
|
4
|
+
|
|
5
|
+
export class BlogPostService extends AbstractService {
|
|
6
|
+
static async findMany(params: { search?: string; next?: NextRef } = {}): Promise<BlogPostInterface[]> {
|
|
7
|
+
const endpoint = new EndpointCreator({ endpoint: Modules.BlogPost });
|
|
8
|
+
|
|
9
|
+
if (params.search) endpoint.addAdditionalParam("search", params.search);
|
|
10
|
+
|
|
11
|
+
return this.callApi<BlogPostInterface[]>({
|
|
12
|
+
type: Modules.BlogPost,
|
|
13
|
+
method: HttpMethod.GET,
|
|
14
|
+
endpoint: endpoint.generate(),
|
|
15
|
+
next: params.next,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static async findOne(params: { id: string }): Promise<BlogPostInterface> {
|
|
20
|
+
return this.callApi<BlogPostInterface>({
|
|
21
|
+
type: Modules.BlogPost,
|
|
22
|
+
method: HttpMethod.GET,
|
|
23
|
+
endpoint: new EndpointCreator({ endpoint: Modules.BlogPost, id: params.id }).generate(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static async create(params: BlogPostInput): Promise<BlogPostInterface> {
|
|
28
|
+
return this.callApi<BlogPostInterface>({
|
|
29
|
+
type: Modules.BlogPost,
|
|
30
|
+
method: HttpMethod.POST,
|
|
31
|
+
endpoint: new EndpointCreator({ endpoint: Modules.BlogPost }).generate(),
|
|
32
|
+
input: params,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static async update(params: BlogPostInput): Promise<BlogPostInterface> {
|
|
37
|
+
return this.callApi<BlogPostInterface>({
|
|
38
|
+
type: Modules.BlogPost,
|
|
39
|
+
method: HttpMethod.PUT,
|
|
40
|
+
endpoint: new EndpointCreator({ endpoint: Modules.BlogPost, id: params.id }).generate(),
|
|
41
|
+
input: params,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static async delete(params: { id: string }): Promise<void> {
|
|
46
|
+
await this.callApi({
|
|
47
|
+
type: Modules.BlogPost,
|
|
48
|
+
method: HttpMethod.DELETE,
|
|
49
|
+
endpoint: new EndpointCreator({ endpoint: Modules.BlogPost, id: params.id }).generate(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* POST blog-posts/:id/publish. Sends no body; replies with the post.
|
|
55
|
+
*/
|
|
56
|
+
static async publish(params: { id: string }): Promise<BlogPostInterface> {
|
|
57
|
+
return this.callApi<BlogPostInterface>({
|
|
58
|
+
type: Modules.BlogPost,
|
|
59
|
+
method: HttpMethod.POST,
|
|
60
|
+
endpoint: new EndpointCreator({
|
|
61
|
+
endpoint: Modules.BlogPost,
|
|
62
|
+
id: params.id,
|
|
63
|
+
childEndpoint: BLOG_ACTION.publish,
|
|
64
|
+
}).generate(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* POST blog-posts/:id/unpublish. Sends no body; replies with the post.
|
|
70
|
+
*/
|
|
71
|
+
static async unpublish(params: { id: string }): Promise<BlogPostInterface> {
|
|
72
|
+
return this.callApi<BlogPostInterface>({
|
|
73
|
+
type: Modules.BlogPost,
|
|
74
|
+
method: HttpMethod.POST,
|
|
75
|
+
endpoint: new EndpointCreator({
|
|
76
|
+
endpoint: Modules.BlogPost,
|
|
77
|
+
id: params.id,
|
|
78
|
+
childEndpoint: BLOG_ACTION.unpublish,
|
|
79
|
+
}).generate(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { ModuleRegistry } from "../../../core/registry/ModuleRegistry";
|
|
3
|
+
import { ModuleFactory, ModuleWithPermissions } from "../../../permissions/types";
|
|
4
|
+
import { BlogPostModule, BlogPostTranslationModule } from "../BlogModules";
|
|
5
|
+
import { BlogPostTranslation } from "./BlogPostTranslation";
|
|
6
|
+
|
|
7
|
+
const moduleFactory: ModuleFactory = (params) =>
|
|
8
|
+
({
|
|
9
|
+
pageUrl: params.pageUrl,
|
|
10
|
+
name: params.name,
|
|
11
|
+
model: params.model,
|
|
12
|
+
}) as ModuleWithPermissions;
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
ModuleRegistry.register("BlogPost", BlogPostModule(moduleFactory));
|
|
16
|
+
ModuleRegistry.register("BlogPostTranslation", BlogPostTranslationModule(moduleFactory));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const blocks = [{ type: "paragraph", content: [{ type: "text", text: "Ciao", styles: {} }] }];
|
|
20
|
+
|
|
21
|
+
describe("BlogPostTranslation", () => {
|
|
22
|
+
it("BlogPostTranslation.rehydrate exposes source, aiStatus and translatedAt as Date", () => {
|
|
23
|
+
const translation = new BlogPostTranslation().rehydrate({
|
|
24
|
+
jsonApi: {
|
|
25
|
+
id: "t",
|
|
26
|
+
type: "blog-post-translations",
|
|
27
|
+
attributes: {
|
|
28
|
+
language: "it",
|
|
29
|
+
isOriginal: false,
|
|
30
|
+
source: "manual",
|
|
31
|
+
aiStatus: "completed",
|
|
32
|
+
title: "Titolo",
|
|
33
|
+
slug: "titolo",
|
|
34
|
+
body: JSON.stringify(blocks),
|
|
35
|
+
readingMinutes: 2,
|
|
36
|
+
translatedAt: "2026-10-12T09:00:00.000Z",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
included: [],
|
|
40
|
+
} as any);
|
|
41
|
+
|
|
42
|
+
expect(translation.language).toBe("it");
|
|
43
|
+
expect(translation.isOriginal).toBe(false);
|
|
44
|
+
expect(translation.source).toBe("manual");
|
|
45
|
+
expect(translation.aiStatus).toBe("completed");
|
|
46
|
+
expect(translation.translatedAt).toBeInstanceOf(Date);
|
|
47
|
+
expect(translation.translatedAt?.toISOString()).toBe("2026-10-12T09:00:00.000Z");
|
|
48
|
+
expect(translation.body).toEqual(blocks);
|
|
49
|
+
expect(translation.readingMinutes).toBe(2);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("a pending placeholder has no text and no translatedAt", () => {
|
|
53
|
+
const translation = new BlogPostTranslation().rehydrate({
|
|
54
|
+
jsonApi: {
|
|
55
|
+
id: "t",
|
|
56
|
+
type: "blog-post-translations",
|
|
57
|
+
attributes: { language: "it", isOriginal: false, source: "ai", aiStatus: "pending" },
|
|
58
|
+
},
|
|
59
|
+
included: [],
|
|
60
|
+
} as any);
|
|
61
|
+
|
|
62
|
+
expect(translation.aiStatus).toBe("pending");
|
|
63
|
+
expect(translation.title).toBe("");
|
|
64
|
+
expect(translation.body).toEqual([]);
|
|
65
|
+
expect(translation.translatedAt).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("createJsonApi stringifies the body and sends only the editable fields", () => {
|
|
69
|
+
const payload = new BlogPostTranslation().createJsonApi({ id: "t", title: "Titolo", body: blocks });
|
|
70
|
+
expect(payload.data.type).toBe("blog-post-translations");
|
|
71
|
+
expect(payload.data.attributes).toEqual({ title: "Titolo", body: JSON.stringify(blocks) });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { BlogAiStatus, BlogTranslationSource } from "../types/blog.constants";
|
|
3
|
+
import { BlogPostInterface } from "./BlogPostInterface";
|
|
4
|
+
import { BlogPostTranslationInput, BlogPostTranslationInterface } from "./BlogPostTranslationInterface";
|
|
5
|
+
|
|
6
|
+
export class BlogPostTranslation extends AbstractApiData implements BlogPostTranslationInterface {
|
|
7
|
+
private _language?: string;
|
|
8
|
+
private _isOriginal?: boolean;
|
|
9
|
+
private _source?: BlogTranslationSource;
|
|
10
|
+
private _aiStatus?: BlogAiStatus;
|
|
11
|
+
private _title?: string;
|
|
12
|
+
private _slug?: string;
|
|
13
|
+
private _summary?: string;
|
|
14
|
+
private _body?: any;
|
|
15
|
+
private _coverImageAlt?: string;
|
|
16
|
+
private _readingMinutes?: number;
|
|
17
|
+
private _translatedAt?: Date;
|
|
18
|
+
private _post?: BlogPostInterface;
|
|
19
|
+
|
|
20
|
+
get language(): string {
|
|
21
|
+
return this._language ?? "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get isOriginal(): boolean {
|
|
25
|
+
return this._isOriginal ?? false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get source(): BlogTranslationSource {
|
|
29
|
+
return this._source ?? "ai";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get aiStatus(): BlogAiStatus {
|
|
33
|
+
return this._aiStatus ?? "pending";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get title(): string {
|
|
37
|
+
return this._title ?? "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get slug(): string {
|
|
41
|
+
return this._slug ?? "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get summary(): string | undefined {
|
|
45
|
+
return this._summary;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get body(): any {
|
|
49
|
+
return this._body ?? [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get coverImageAlt(): string | undefined {
|
|
53
|
+
return this._coverImageAlt;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get readingMinutes(): number {
|
|
57
|
+
return this._readingMinutes ?? 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get translatedAt(): Date | undefined {
|
|
61
|
+
return this._translatedAt;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get post(): BlogPostInterface | undefined {
|
|
65
|
+
return this._post;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
69
|
+
super.rehydrate(data);
|
|
70
|
+
|
|
71
|
+
const attributes = data.jsonApi.attributes ?? {};
|
|
72
|
+
|
|
73
|
+
this._language = attributes.language;
|
|
74
|
+
this._isOriginal = attributes.isOriginal === true;
|
|
75
|
+
this._source = attributes.source;
|
|
76
|
+
this._aiStatus = attributes.aiStatus;
|
|
77
|
+
this._title = attributes.title ?? undefined;
|
|
78
|
+
this._slug = attributes.slug ?? undefined;
|
|
79
|
+
this._summary = attributes.summary ?? undefined;
|
|
80
|
+
this._body = attributes.body ? JSON.parse(attributes.body) : undefined;
|
|
81
|
+
this._coverImageAlt = attributes.coverImageAlt ?? undefined;
|
|
82
|
+
this._readingMinutes = attributes.readingMinutes ?? undefined;
|
|
83
|
+
this._translatedAt = attributes.translatedAt ? new Date(attributes.translatedAt) : undefined;
|
|
84
|
+
|
|
85
|
+
this._post = this._readIncluded(data, "post", Modules.BlogPost) as BlogPostInterface;
|
|
86
|
+
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
createJsonApi(data: BlogPostTranslationInput) {
|
|
91
|
+
const response: any = {
|
|
92
|
+
data: {
|
|
93
|
+
type: Modules.BlogPostTranslation.name,
|
|
94
|
+
id: data.id,
|
|
95
|
+
attributes: {},
|
|
96
|
+
meta: {},
|
|
97
|
+
relationships: {},
|
|
98
|
+
},
|
|
99
|
+
included: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (data.title !== undefined) response.data.attributes.title = data.title;
|
|
103
|
+
if (data.slug !== undefined) response.data.attributes.slug = data.slug;
|
|
104
|
+
if (data.summary !== undefined) response.data.attributes.summary = data.summary;
|
|
105
|
+
if (data.body !== undefined) response.data.attributes.body = JSON.stringify(data.body);
|
|
106
|
+
if (data.coverImageAlt !== undefined) response.data.attributes.coverImageAlt = data.coverImageAlt;
|
|
107
|
+
|
|
108
|
+
return response;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
import { BlogAiStatus, BlogTranslationSource } from "../types/blog.constants";
|
|
3
|
+
import { BlogPostInterface } from "./BlogPostInterface";
|
|
4
|
+
|
|
5
|
+
export type BlogPostTranslationInput = {
|
|
6
|
+
id: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
slug?: string;
|
|
9
|
+
summary?: string;
|
|
10
|
+
body?: any;
|
|
11
|
+
coverImageAlt?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface BlogPostTranslationInterface extends ApiDataInterface {
|
|
15
|
+
get language(): string;
|
|
16
|
+
get isOriginal(): boolean;
|
|
17
|
+
get source(): BlogTranslationSource;
|
|
18
|
+
get aiStatus(): BlogAiStatus;
|
|
19
|
+
get title(): string;
|
|
20
|
+
get slug(): string;
|
|
21
|
+
get summary(): string | undefined;
|
|
22
|
+
get body(): any;
|
|
23
|
+
get coverImageAlt(): string | undefined;
|
|
24
|
+
get readingMinutes(): number;
|
|
25
|
+
get translatedAt(): Date | undefined;
|
|
26
|
+
get post(): BlogPostInterface | undefined;
|
|
27
|
+
}
|