@intelex-ia/sdk 0.1.0 → 0.2.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/README.md +173 -70
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/merge.d.ts +86 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +84 -0
- package/dist/merge.js.map +1 -0
- package/dist/merge.test.d.ts +2 -0
- package/dist/merge.test.d.ts.map +1 -0
- package/dist/merge.test.js +224 -0
- package/dist/merge.test.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,76 +2,158 @@
|
|
|
2
2
|
|
|
3
3
|
SDK pra consumir a **Content API IntelexIA** em sites headless (Next.js, Astro etc.). Multiplataforma Fase 2b.
|
|
4
4
|
|
|
5
|
-
> **O que você ganha:** `getArticles` / `getArticleBySlug` tipados,
|
|
5
|
+
> **O que você ganha:** `getArticles` / `getArticleBySlug` tipados, `mergePosts()` pra unir conteúdo antigo + novo do painel, `<ArticleJsonLd>` pra injetar schema JSON-LD, e `articleToMetadata` pra plugar no `generateMetadata` do Next.js App Router. Zero dependências (usa `fetch` global).
|
|
6
6
|
|
|
7
|
-
## Instalação
|
|
7
|
+
## Instalação rápida (recomendado)
|
|
8
|
+
|
|
9
|
+
Se você está começando agora, use o CLI — ele detecta a estrutura do seu projeto e configura tudo automaticamente:
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
|
|
11
|
-
# ou
|
|
12
|
-
npm install @intelex-ia/sdk
|
|
12
|
+
npx @intelex-ia/cli init
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Cria os arquivos no lugar certo (App Router ou Pages Router, com ou sem `src/`), configura `.env.local`, gera um `INTELEXIA.md` completo na raiz e instala o SDK pra você.
|
|
16
|
+
|
|
17
|
+
## Instalação manual
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @intelex-ia/sdk
|
|
21
|
+
# ou pnpm add @intelex-ia/sdk / yarn add @intelex-ia/sdk
|
|
22
|
+
```
|
|
16
23
|
|
|
17
24
|
## Configuração
|
|
18
25
|
|
|
19
|
-
1. No painel da IntelexIA, **cadastre o site como "Headless (código)"** (vai pedir
|
|
26
|
+
1. No painel da IntelexIA, **cadastre o site como "Headless (código)"** (vai pedir domain + URL e secret de revalidate).
|
|
20
27
|
2. Abra o site na lista → seção **"Tokens de API (Content API)"** → **Gerar token**. Você recebe `itx_…` (mostrado uma única vez).
|
|
21
|
-
3. Coloque
|
|
28
|
+
3. Coloque em `.env.local`:
|
|
22
29
|
|
|
23
30
|
```bash
|
|
24
|
-
|
|
31
|
+
INTELEXIA_API_TOKEN=itx_seu_token_aqui
|
|
25
32
|
INTELEXIA_API_URL=https://api.innovarticles.intelexia.com.br # opcional, é o default
|
|
33
|
+
INTELEXIA_REVALIDATE_SECRET=cole-o-mesmo-segredo-do-painel
|
|
26
34
|
```
|
|
27
35
|
|
|
28
|
-
##
|
|
36
|
+
## Três modos de integração
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
Cada cliente tem uma situação diferente. Escolha o modo que faz sentido:
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
| Modo | Quando usar | O que muda no `/blog` |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **🔀 Additive (recomendado)** | Cliente já tem posts antigos e quer **adicionar** os do painel sem perder os antigos | `mergePosts()` une os dois feeds, dedupa por slug, ordena por data |
|
|
43
|
+
| **🔁 Replace** | Cliente migrou tudo pro painel — só os novos contam | Substitui `getAllPosts()` por `client.getArticles()` |
|
|
44
|
+
| **🆕 Isolated** | Cliente quer manter `/blog` antigo intacto e criar rota nova só pra IA | Cria `/blog/ia` separado, blog antigo zero alterado |
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
token: process.env.INTELEXIA_TOKEN!,
|
|
37
|
-
// ISR: revalida a cada 60s (ou use webhook revalidate pra invalidar
|
|
38
|
-
// sob demanda — ver mais abaixo).
|
|
39
|
-
init: { next: { revalidate: 60, tags: ["intelexia-articles"] } },
|
|
40
|
-
});
|
|
41
|
-
```
|
|
46
|
+
---
|
|
42
47
|
|
|
43
|
-
###
|
|
48
|
+
### Modo 1: Additive (cliente existente — caso mais comum)
|
|
44
49
|
|
|
45
50
|
```tsx
|
|
51
|
+
// app/blog/page.tsx
|
|
46
52
|
import { intelexia } from "@/lib/intelexia";
|
|
53
|
+
import { mergePosts } from "@intelex-ia/sdk";
|
|
54
|
+
import { posts as localPosts } from "@/lib/posts"; // seu array atual
|
|
47
55
|
import Link from "next/link";
|
|
48
56
|
|
|
49
57
|
export default async function BlogIndex() {
|
|
50
|
-
|
|
58
|
+
// 1) Busca os artigos do painel
|
|
59
|
+
const { items: remote } = await intelexia.getArticles({ limit: 50 });
|
|
60
|
+
|
|
61
|
+
// 2) Mistura com os seus posts antigos
|
|
62
|
+
const all = mergePosts({
|
|
63
|
+
local: localPosts,
|
|
64
|
+
remote,
|
|
65
|
+
mapLocal: (p) => ({
|
|
66
|
+
slug: p.slug,
|
|
67
|
+
title: p.title,
|
|
68
|
+
excerpt: p.excerpt,
|
|
69
|
+
publishedAt: p.date,
|
|
70
|
+
featuredImageUrl: null,
|
|
71
|
+
category: p.category,
|
|
72
|
+
}),
|
|
73
|
+
// Opcional — defaults sensatos:
|
|
74
|
+
// dedupe: "remote-wins" (painel substitui local se slug colide)
|
|
75
|
+
// sortBy: "date-desc" (mais recente primeiro)
|
|
76
|
+
});
|
|
77
|
+
|
|
51
78
|
return (
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</li>
|
|
62
|
-
))}
|
|
63
|
-
</ul>
|
|
64
|
-
</main>
|
|
79
|
+
<ul>
|
|
80
|
+
{all.map((p) => (
|
|
81
|
+
<li key={p.slug}>
|
|
82
|
+
<Link href={`/blog/${p.slug}`}>{p.title}</Link>
|
|
83
|
+
<time>{new Date(p.publishedAt).toLocaleDateString("pt-BR")}</time>
|
|
84
|
+
{p.source === "remote" && <span className="badge-ia">✨ IA</span>}
|
|
85
|
+
</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
65
88
|
);
|
|
66
89
|
}
|
|
67
90
|
```
|
|
68
91
|
|
|
69
|
-
|
|
92
|
+
Pro post individual, tenta o painel primeiro, cai pro local se 404:
|
|
70
93
|
|
|
71
94
|
```tsx
|
|
95
|
+
// app/blog/[slug]/page.tsx
|
|
72
96
|
import { intelexia } from "@/lib/intelexia";
|
|
97
|
+
import { IntelexiaApiError } from "@intelex-ia/sdk";
|
|
73
98
|
import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
|
|
99
|
+
import { getPostBySlug } from "@/lib/posts";
|
|
100
|
+
import { notFound } from "next/navigation";
|
|
101
|
+
|
|
102
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
103
|
+
const { slug } = await params;
|
|
104
|
+
|
|
105
|
+
// 1) Tenta painel
|
|
106
|
+
try {
|
|
107
|
+
const { article } = await intelexia.getArticleBySlug(slug);
|
|
108
|
+
return (
|
|
109
|
+
<article>
|
|
110
|
+
<ArticleJsonLd article={article} />
|
|
111
|
+
<h1>{article.title}</h1>
|
|
112
|
+
<div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
|
|
113
|
+
</article>
|
|
114
|
+
);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (!(err instanceof IntelexiaApiError) || err.status !== 404) throw err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 2) Fallback: post antigo local
|
|
120
|
+
const local = getPostBySlug(slug);
|
|
121
|
+
if (!local) notFound();
|
|
122
|
+
return (
|
|
123
|
+
<article>
|
|
124
|
+
<h1>{local.title}</h1>
|
|
125
|
+
{/* renderiza o local do jeito que você já fazia */}
|
|
126
|
+
</article>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Modo 2: Replace (migração completa)
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// app/blog/page.tsx
|
|
135
|
+
import { intelexia } from "@/lib/intelexia";
|
|
136
|
+
|
|
137
|
+
export default async function BlogIndex() {
|
|
138
|
+
const { items } = await intelexia.getArticles({ limit: 20 });
|
|
139
|
+
return (
|
|
140
|
+
<ul>
|
|
141
|
+
{items.map((a) => (
|
|
142
|
+
<li key={a.id}>
|
|
143
|
+
<Link href={`/blog/${a.slug}`}>{a.title}</Link>
|
|
144
|
+
</li>
|
|
145
|
+
))}
|
|
146
|
+
</ul>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Pro post individual, sem fallback:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { intelexia } from "@/lib/intelexia";
|
|
74
155
|
import { IntelexiaApiError } from "@intelex-ia/sdk";
|
|
156
|
+
import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
|
|
75
157
|
import { notFound } from "next/navigation";
|
|
76
158
|
|
|
77
159
|
async function fetchArticle(slug: string) {
|
|
@@ -83,50 +165,52 @@ async function fetchArticle(slug: string) {
|
|
|
83
165
|
}
|
|
84
166
|
}
|
|
85
167
|
|
|
86
|
-
export async function generateMetadata({
|
|
87
|
-
params,
|
|
88
|
-
}: {
|
|
89
|
-
params: Promise<{ slug: string }>;
|
|
90
|
-
}) {
|
|
168
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
|
|
91
169
|
const { slug } = await params;
|
|
92
170
|
const { article } = await fetchArticle(slug);
|
|
93
|
-
return articleToMetadata(article);
|
|
171
|
+
return articleToMetadata(article);
|
|
94
172
|
}
|
|
95
173
|
|
|
96
|
-
export default async function PostPage({
|
|
97
|
-
params,
|
|
98
|
-
}: {
|
|
99
|
-
params: Promise<{ slug: string }>;
|
|
100
|
-
}) {
|
|
174
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
101
175
|
const { slug } = await params;
|
|
102
176
|
const { article } = await fetchArticle(slug);
|
|
103
177
|
return (
|
|
104
178
|
<article>
|
|
105
|
-
{/* Schema JSON-LD (Article/Breadcrumb/FAQ/HowTo/Org/Person) */}
|
|
106
179
|
<ArticleJsonLd article={article} />
|
|
107
|
-
|
|
108
180
|
<h1>{article.title}</h1>
|
|
109
|
-
{article.
|
|
110
|
-
<img src={article.featuredImageUrl} alt={article.title ?? ""} />
|
|
111
|
-
)}
|
|
112
|
-
<div
|
|
113
|
-
dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }}
|
|
114
|
-
/>
|
|
181
|
+
<div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
|
|
115
182
|
</article>
|
|
116
183
|
);
|
|
117
184
|
}
|
|
118
185
|
```
|
|
119
186
|
|
|
120
|
-
###
|
|
187
|
+
### Modo 3: Isolated (rota nova, blog antigo intacto)
|
|
188
|
+
|
|
189
|
+
Cria `app/blog/ia/page.tsx` e `app/blog/ia/[slug]/page.tsx` com o mesmo código do modo Replace, mas em rota separada (`/blog/ia` em vez de `/blog`). Zero alteração no blog existente.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Cliente — `lib/intelexia.ts`
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { createClient } from "@intelex-ia/sdk";
|
|
197
|
+
|
|
198
|
+
export const intelexia = createClient({
|
|
199
|
+
token: process.env.INTELEXIA_API_TOKEN!,
|
|
200
|
+
// ISR: revalida a cada 60s; o webhook revalidate invalida sob demanda
|
|
201
|
+
init: { next: { revalidate: 60, tags: ["intelexia-articles"] } },
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Webhook de revalidate — `app/api/revalidate/route.ts`
|
|
121
206
|
|
|
122
|
-
Quando um artigo publica no painel,
|
|
207
|
+
Quando um artigo publica/edita no painel, nosso worker faz `POST` no seu endpoint. Implemente assim:
|
|
123
208
|
|
|
124
209
|
```ts
|
|
125
210
|
import { NextResponse } from "next/server";
|
|
126
211
|
import { revalidatePath, revalidateTag } from "next/cache";
|
|
127
212
|
|
|
128
213
|
export async function POST(req: Request) {
|
|
129
|
-
// Autentica com o secret cadastrado no painel (header X ou body)
|
|
130
214
|
const headerSecret = req.headers.get("x-revalidate-secret");
|
|
131
215
|
const body = (await req.json().catch(() => ({}))) as {
|
|
132
216
|
secret?: string;
|
|
@@ -138,7 +222,6 @@ export async function POST(req: Request) {
|
|
|
138
222
|
return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
|
|
139
223
|
}
|
|
140
224
|
|
|
141
|
-
// Revalida a página do post (se veio slug) e o índice do blog.
|
|
142
225
|
if (body.slug) revalidatePath(`/blog/${body.slug}`);
|
|
143
226
|
revalidatePath("/blog");
|
|
144
227
|
revalidateTag("intelexia-articles");
|
|
@@ -147,13 +230,7 @@ export async function POST(req: Request) {
|
|
|
147
230
|
}
|
|
148
231
|
```
|
|
149
232
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
INTELEXIA_REVALIDATE_SECRET=cole-o-mesmo-segredo-do-painel
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
> O worker envia o secret tanto em `x-revalidate-secret` quanto no body (`{ secret, slug, domain }`), tem timeout de 15s e é **fire-and-forget** (se o seu endpoint falhar, o artigo continua publicado e a ISR atualiza pelo `revalidate: 60`).
|
|
233
|
+
> O worker envia o secret tanto no header `x-revalidate-secret` quanto no body, tem timeout de 15s e é **fire-and-forget** (se seu endpoint falhar, o artigo continua publicado e a ISR atualiza pelo `revalidate: 60`).
|
|
157
234
|
|
|
158
235
|
## API
|
|
159
236
|
|
|
@@ -174,6 +251,18 @@ Resposta: `{ ok, mode, lifecycle, count, items: ArticleListItem[] }`.
|
|
|
174
251
|
|
|
175
252
|
Resposta: `{ ok, mode, lifecycle, article: Article }`. **404 → joga `IntelexiaApiError`** (use `try/catch` + `notFound()`).
|
|
176
253
|
|
|
254
|
+
### `mergePosts({ local, remote, mapLocal, dedupe?, sortBy? })` 🆕 0.2.0
|
|
255
|
+
|
|
256
|
+
| Opção | Tipo | Default |
|
|
257
|
+
| ---------- | ------------------------------------------------- | ---------------- |
|
|
258
|
+
| `local` | `readonly TLocal[]` (qualquer shape) | — |
|
|
259
|
+
| `remote` | `readonly ArticleListItem[]` | — |
|
|
260
|
+
| `mapLocal` | `(post: TLocal) => Omit<UnifiedPost,"source"> \| null` | — |
|
|
261
|
+
| `dedupe` | `"remote-wins" \| "local-wins" \| "throw"` | `"remote-wins"` |
|
|
262
|
+
| `sortBy` | `"date-desc" \| "date-asc" \| "none"` | `"date-desc"` |
|
|
263
|
+
|
|
264
|
+
Retorna `UnifiedPost[]` com `source: "local" | "remote"`. `mapLocal` retornando `null` filtra o post (útil pra esconder rascunhos). Slugs duplicados resolvidos pela estratégia `dedupe`. Veja exemplo no Modo Additive acima.
|
|
265
|
+
|
|
177
266
|
### `mode: "full" | "plain"`
|
|
178
267
|
|
|
179
268
|
- **`full`** (sites `active`/`paused`): inclui schema JSON-LD, metaTitle/Description, keyword(s), interlinking interno no `contentHtml`.
|
|
@@ -189,7 +278,7 @@ A decisão é **server-side e não-negociável** — o cliente não escolhe o mo
|
|
|
189
278
|
## Erros
|
|
190
279
|
|
|
191
280
|
```ts
|
|
192
|
-
import { IntelexiaApiError } from "@intelex-ia/sdk";
|
|
281
|
+
import { IntelexiaApiError, MergeConflictError } from "@intelex-ia/sdk";
|
|
193
282
|
|
|
194
283
|
try {
|
|
195
284
|
await intelexia.getArticleBySlug("post-inexistente");
|
|
@@ -198,10 +287,24 @@ try {
|
|
|
198
287
|
console.error(err.status, err.message); // 404, "Artigo não encontrado."
|
|
199
288
|
}
|
|
200
289
|
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
mergePosts({ local, remote, mapLocal, dedupe: "throw" });
|
|
293
|
+
} catch (err) {
|
|
294
|
+
if (err instanceof MergeConflictError) {
|
|
295
|
+
console.error(`Conflito de slug: ${err.slug}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
201
298
|
```
|
|
202
299
|
|
|
203
300
|
## Compatibilidade
|
|
204
301
|
|
|
205
|
-
- Node 18+ (fetch nativo), Edge Runtime, Browser.
|
|
206
|
-
- React 18+ (peer dep opcional — só pros helpers `/react`).
|
|
207
|
-
- TypeScript
|
|
302
|
+
- **Node** 18+ (fetch nativo), Edge Runtime, Browser.
|
|
303
|
+
- **React** 18+ (peer dep opcional — só pros helpers `/react`).
|
|
304
|
+
- **TypeScript:** tipos publicados no `dist`.
|
|
305
|
+
- **Frameworks:** Next.js (App Router e Pages Router), Astro, Remix, SvelteKit, Vite, qualquer JS.
|
|
306
|
+
|
|
307
|
+
## Changelog
|
|
308
|
+
|
|
309
|
+
- **0.2.0** — adiciona `mergePosts()` + tipo `UnifiedPost` + `MergeConflictError` (3 modos de integração).
|
|
310
|
+
- **0.1.0** — release inicial: `createClient`, `getArticles`, `getArticleBySlug`, `<ArticleJsonLd>`, `articleToMetadata`.
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { createClient, IntelexiaApiError, type ClientConfig, type ArticlesListResponse, type ArticleResponse, type IntelexiaClient, } from "./client.js";
|
|
8
8
|
export type { Lifecycle, Mode, Article, ArticleListItem, CustomMetaTag, ErrorResponse, } from "./types.js";
|
|
9
|
+
export { mergePosts, MergeConflictError, type UnifiedPost, type MergePostsOptions, } from "./merge.js";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,SAAS,EACT,IAAI,EACJ,OAAO,EACP,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,SAAS,EACT,IAAI,EACJ,OAAO,EACP,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,7 @@
|
|
|
5
5
|
* `@intelex-ia/sdk/react`.
|
|
6
6
|
*/
|
|
7
7
|
export { createClient, IntelexiaApiError, } from "./client.js";
|
|
8
|
+
// Merge helpers (0.2.0+) — pro caso real "cliente já tem posts antigos
|
|
9
|
+
// e quer adicionar os do painel sem perder os antigos".
|
|
10
|
+
export { mergePosts, MergeConflictError, } from "./merge.js";
|
|
8
11
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,GAKlB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,GAKlB,MAAM,aAAa,CAAC;AAWrB,uEAAuE;AACvE,wDAAwD;AACxD,OAAO,EACL,UAAU,EACV,kBAAkB,GAGnB,MAAM,YAAY,CAAC"}
|
package/dist/merge.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mergePosts()` — junta posts locais (formato do cliente) com artigos do
|
|
3
|
+
* painel IntelexIA num feed unificado. Resolve o caso real "cliente já tem
|
|
4
|
+
* 50 artigos no site e quer ADICIONAR os da IA sem perder os antigos".
|
|
5
|
+
*
|
|
6
|
+
* Função pura, zero deps, zero React. Roda em qualquer ambiente JS.
|
|
7
|
+
*
|
|
8
|
+
* @example modo "additive" (clientes existentes — caso mais comum)
|
|
9
|
+
* import { listArticles } from "@/lib/intelexia"
|
|
10
|
+
* import { mergePosts } from "@intelex-ia/sdk"
|
|
11
|
+
*
|
|
12
|
+
* const { items: remote } = await client.getArticles({ limit: 50 })
|
|
13
|
+
* const merged = mergePosts({
|
|
14
|
+
* local: localPosts,
|
|
15
|
+
* remote,
|
|
16
|
+
* mapLocal: (p) => ({
|
|
17
|
+
* slug: p.slug,
|
|
18
|
+
* title: p.title,
|
|
19
|
+
* excerpt: p.excerpt,
|
|
20
|
+
* publishedAt: p.date,
|
|
21
|
+
* category: p.category,
|
|
22
|
+
* }),
|
|
23
|
+
* })
|
|
24
|
+
* // merged: UnifiedPost[] — ordenado por data desc, slug duplicado: remoto vence
|
|
25
|
+
*/
|
|
26
|
+
import type { ArticleListItem } from "./types.js";
|
|
27
|
+
/**
|
|
28
|
+
* Shape unificado pra renderizar lista de posts independente da origem.
|
|
29
|
+
* Cliente que precisa de mais campos pode estender via TypeScript generics
|
|
30
|
+
* — por enquanto fixamos o mínimo viável pra UI (slug + meta de card).
|
|
31
|
+
*/
|
|
32
|
+
export interface UnifiedPost {
|
|
33
|
+
/** Slug pra URL. Único no feed (dedup por slug). */
|
|
34
|
+
slug: string;
|
|
35
|
+
/** Título do post. */
|
|
36
|
+
title: string;
|
|
37
|
+
/** Resumo curto (excerpt/metaDescription). Pode ser `null` se ausente. */
|
|
38
|
+
excerpt: string | null;
|
|
39
|
+
/** Data ISO 8601 (`2026-05-26T10:00:00Z` ou `2026-05-26`). */
|
|
40
|
+
publishedAt: string;
|
|
41
|
+
/** URL absoluta da imagem destacada. `null` se não tiver. */
|
|
42
|
+
featuredImageUrl: string | null;
|
|
43
|
+
/** Categoria principal (string display). `null` se não classificado. */
|
|
44
|
+
category: string | null;
|
|
45
|
+
/** De onde veio: `"local"` (cliente) ou `"remote"` (painel IntelexIA). */
|
|
46
|
+
source: "local" | "remote";
|
|
47
|
+
}
|
|
48
|
+
/** Opções de `mergePosts()`. `local` é genérico — cliente passa qualquer shape. */
|
|
49
|
+
export interface MergePostsOptions<TLocal> {
|
|
50
|
+
/** Array de posts locais (formato livre do cliente). */
|
|
51
|
+
local: readonly TLocal[];
|
|
52
|
+
/** Array de artigos vindos da Content API (via `client.getArticles()`). */
|
|
53
|
+
remote: readonly ArticleListItem[];
|
|
54
|
+
/**
|
|
55
|
+
* Mapper que converte um post local pro `UnifiedPost`. Obrigatório porque
|
|
56
|
+
* cada cliente tem um shape diferente (markdown frontmatter, CMS custom,
|
|
57
|
+
* array hardcoded, etc.). Retorne `null` pra ignorar o post (filtro).
|
|
58
|
+
*/
|
|
59
|
+
mapLocal: (post: TLocal) => Omit<UnifiedPost, "source"> | null;
|
|
60
|
+
/**
|
|
61
|
+
* Estratégia quando um slug existe nos DOIS lados:
|
|
62
|
+
* - `"remote-wins"` (default): painel substitui o local. Faz sentido pra
|
|
63
|
+
* migração gradual (cliente publica versão nova IA do post antigo).
|
|
64
|
+
* - `"local-wins"`: ignora o remoto, mantém o local.
|
|
65
|
+
* - `"throw"`: lança `MergeConflictError` (forçar resolução explícita).
|
|
66
|
+
*/
|
|
67
|
+
dedupe?: "remote-wins" | "local-wins" | "throw";
|
|
68
|
+
/**
|
|
69
|
+
* Ordenação do feed final:
|
|
70
|
+
* - `"date-desc"` (default): mais recente primeiro.
|
|
71
|
+
* - `"date-asc"`: mais antigo primeiro.
|
|
72
|
+
* - `"none"`: mantém ordem (remotos primeiro, locais depois — útil pra
|
|
73
|
+
* "destaques curados" no topo).
|
|
74
|
+
*/
|
|
75
|
+
sortBy?: "date-desc" | "date-asc" | "none";
|
|
76
|
+
}
|
|
77
|
+
/** Lançado quando `dedupe: "throw"` e há slug duplicado. */
|
|
78
|
+
export declare class MergeConflictError extends Error {
|
|
79
|
+
readonly slug: string;
|
|
80
|
+
constructor(slug: string);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Junta posts locais + remotos num único feed. Veja `MergePostsOptions`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function mergePosts<TLocal>(opts: MergePostsOptions<TLocal>): UnifiedPost[];
|
|
86
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,wEAAwE;IACxE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,0EAA0E;IAC1E,MAAM,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC5B;AAED,mFAAmF;AACnF,MAAM,WAAW,iBAAiB,CAAC,MAAM;IACvC,wDAAwD;IACxD,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,2EAA2E;IAC3E,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC;;;;OAIG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAC;IAC/D;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,aAAa,GAAG,YAAY,GAAG,OAAO,CAAC;IAChD;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,CAAC;CAC5C;AAED,4DAA4D;AAC5D,qBAAa,kBAAmB,SAAQ,KAAK;aACf,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAOzC;AAmBD;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAC/B,IAAI,EAAE,iBAAiB,CAAC,MAAM,CAAC,GAC9B,WAAW,EAAE,CAoDf"}
|
package/dist/merge.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Lançado quando `dedupe: "throw"` e há slug duplicado. */
|
|
2
|
+
export class MergeConflictError extends Error {
|
|
3
|
+
slug;
|
|
4
|
+
constructor(slug) {
|
|
5
|
+
super(`[@intelex-ia/sdk] mergePosts: slug "${slug}" existe em local E remote. ` +
|
|
6
|
+
`Configure dedupe: "remote-wins" ou "local-wins" pra resolver.`);
|
|
7
|
+
this.slug = slug;
|
|
8
|
+
this.name = "MergeConflictError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Normaliza `ArticleListItem` (Content API) pro `UnifiedPost`. Filtra itens
|
|
13
|
+
* sem slug/title/publishedAt (não dá pra renderizar card).
|
|
14
|
+
*/
|
|
15
|
+
function remoteToUnified(item) {
|
|
16
|
+
if (!item.slug || !item.title || !item.publishedAt)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
slug: item.slug,
|
|
20
|
+
title: item.title,
|
|
21
|
+
excerpt: item.metaDescription ?? null,
|
|
22
|
+
publishedAt: item.publishedAt,
|
|
23
|
+
featuredImageUrl: item.featuredImageUrl,
|
|
24
|
+
category: null, // Content API não devolve categoria na listagem (Fase 2b inc1 — pode evoluir)
|
|
25
|
+
source: "remote",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Junta posts locais + remotos num único feed. Veja `MergePostsOptions`.
|
|
30
|
+
*/
|
|
31
|
+
export function mergePosts(opts) {
|
|
32
|
+
const dedupeMode = opts.dedupe ?? "remote-wins";
|
|
33
|
+
const sortMode = opts.sortBy ?? "date-desc";
|
|
34
|
+
// Indexa por slug pra dedup eficiente.
|
|
35
|
+
const bySlug = new Map();
|
|
36
|
+
// 1) Adiciona LOCAIS primeiro.
|
|
37
|
+
for (const localPost of opts.local) {
|
|
38
|
+
const mapped = opts.mapLocal(localPost);
|
|
39
|
+
if (!mapped)
|
|
40
|
+
continue; // mapper retornou null = filtro
|
|
41
|
+
if (!mapped.slug)
|
|
42
|
+
continue; // post sem slug é ilegível
|
|
43
|
+
bySlug.set(mapped.slug, { ...mapped, source: "local" });
|
|
44
|
+
}
|
|
45
|
+
// 2) Adiciona REMOTOS aplicando estratégia de dedup.
|
|
46
|
+
for (const remoteItem of opts.remote) {
|
|
47
|
+
const unified = remoteToUnified(remoteItem);
|
|
48
|
+
if (!unified)
|
|
49
|
+
continue;
|
|
50
|
+
const existing = bySlug.get(unified.slug);
|
|
51
|
+
if (!existing) {
|
|
52
|
+
bySlug.set(unified.slug, unified);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Conflito: existe local com mesmo slug.
|
|
56
|
+
switch (dedupeMode) {
|
|
57
|
+
case "remote-wins":
|
|
58
|
+
bySlug.set(unified.slug, unified);
|
|
59
|
+
break;
|
|
60
|
+
case "local-wins":
|
|
61
|
+
// mantém o que já tá lá (local), descarta remoto
|
|
62
|
+
break;
|
|
63
|
+
case "throw":
|
|
64
|
+
throw new MergeConflictError(unified.slug);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 3) Ordena.
|
|
68
|
+
const all = Array.from(bySlug.values());
|
|
69
|
+
if (sortMode === "none")
|
|
70
|
+
return all;
|
|
71
|
+
return all.sort((a, b) => {
|
|
72
|
+
const dateA = new Date(a.publishedAt).getTime();
|
|
73
|
+
const dateB = new Date(b.publishedAt).getTime();
|
|
74
|
+
// Datas inválidas vão pro final (NaN comparisons sempre false).
|
|
75
|
+
if (Number.isNaN(dateA) && Number.isNaN(dateB))
|
|
76
|
+
return 0;
|
|
77
|
+
if (Number.isNaN(dateA))
|
|
78
|
+
return 1;
|
|
79
|
+
if (Number.isNaN(dateB))
|
|
80
|
+
return -1;
|
|
81
|
+
return sortMode === "date-desc" ? dateB - dateA : dateA - dateB;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.test.d.ts","sourceRoot":"","sources":["../src/merge.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mergePosts, MergeConflictError } from "./merge.js";
|
|
3
|
+
const localFixture = [
|
|
4
|
+
{
|
|
5
|
+
slug: "post-antigo-2024",
|
|
6
|
+
title: "Post antigo de 2024",
|
|
7
|
+
excerpt: "Resumo do antigo",
|
|
8
|
+
date: "2024-01-15",
|
|
9
|
+
category: "Geral",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
slug: "post-antigo-2025",
|
|
13
|
+
title: "Post antigo de 2025",
|
|
14
|
+
excerpt: "Resumo de 2025",
|
|
15
|
+
date: "2025-06-10",
|
|
16
|
+
category: "Geral",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
const remoteFixture = [
|
|
20
|
+
{
|
|
21
|
+
id: "uuid-1",
|
|
22
|
+
slug: "novo-post-ia",
|
|
23
|
+
title: "Novo post gerado por IA",
|
|
24
|
+
featuredImageUrl: "https://cdn.ex.com/1.jpg",
|
|
25
|
+
words: 1200,
|
|
26
|
+
publishedAt: "2026-05-20T10:00:00Z",
|
|
27
|
+
metaDescription: "Resumo SEO do post novo",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "uuid-2",
|
|
31
|
+
slug: "outro-novo-ia",
|
|
32
|
+
title: "Outro post novo",
|
|
33
|
+
featuredImageUrl: null,
|
|
34
|
+
words: 800,
|
|
35
|
+
publishedAt: "2026-04-15T10:00:00Z",
|
|
36
|
+
metaDescription: "Outro resumo",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const mapLocal = (p) => ({
|
|
40
|
+
slug: p.slug,
|
|
41
|
+
title: p.title,
|
|
42
|
+
excerpt: p.excerpt,
|
|
43
|
+
publishedAt: p.date,
|
|
44
|
+
category: p.category,
|
|
45
|
+
featuredImageUrl: null,
|
|
46
|
+
});
|
|
47
|
+
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
48
|
+
describe("mergePosts", () => {
|
|
49
|
+
it("merge feliz — sem conflito, ordenado date-desc (default)", () => {
|
|
50
|
+
const result = mergePosts({
|
|
51
|
+
local: localFixture,
|
|
52
|
+
remote: remoteFixture,
|
|
53
|
+
mapLocal,
|
|
54
|
+
});
|
|
55
|
+
expect(result).toHaveLength(4);
|
|
56
|
+
// Mais recente primeiro
|
|
57
|
+
expect(result.map((p) => p.slug)).toEqual([
|
|
58
|
+
"novo-post-ia", // 2026-05-20 (remoto)
|
|
59
|
+
"outro-novo-ia", // 2026-04-15 (remoto)
|
|
60
|
+
"post-antigo-2025", // 2025-06-10 (local)
|
|
61
|
+
"post-antigo-2024", // 2024-01-15 (local)
|
|
62
|
+
]);
|
|
63
|
+
// Source flag preservado
|
|
64
|
+
expect(result[0]?.source).toBe("remote");
|
|
65
|
+
expect(result[3]?.source).toBe("local");
|
|
66
|
+
});
|
|
67
|
+
it("ordena date-asc quando solicitado", () => {
|
|
68
|
+
const result = mergePosts({
|
|
69
|
+
local: localFixture,
|
|
70
|
+
remote: remoteFixture,
|
|
71
|
+
mapLocal,
|
|
72
|
+
sortBy: "date-asc",
|
|
73
|
+
});
|
|
74
|
+
expect(result.map((p) => p.slug)).toEqual([
|
|
75
|
+
"post-antigo-2024",
|
|
76
|
+
"post-antigo-2025",
|
|
77
|
+
"outro-novo-ia",
|
|
78
|
+
"novo-post-ia",
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
it("sortBy 'none' mantém ordem (locais primeiro, depois remotos)", () => {
|
|
82
|
+
const result = mergePosts({
|
|
83
|
+
local: localFixture,
|
|
84
|
+
remote: remoteFixture,
|
|
85
|
+
mapLocal,
|
|
86
|
+
sortBy: "none",
|
|
87
|
+
});
|
|
88
|
+
expect(result.map((p) => p.slug)).toEqual([
|
|
89
|
+
"post-antigo-2024",
|
|
90
|
+
"post-antigo-2025",
|
|
91
|
+
"novo-post-ia",
|
|
92
|
+
"outro-novo-ia",
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
it("dedupe 'remote-wins' (default) — remoto substitui local com mesmo slug", () => {
|
|
96
|
+
const localCom = [...localFixture, { ...localFixture[0], slug: "novo-post-ia", title: "Versão local antiga" }];
|
|
97
|
+
const result = mergePosts({
|
|
98
|
+
local: localCom,
|
|
99
|
+
remote: remoteFixture,
|
|
100
|
+
mapLocal,
|
|
101
|
+
});
|
|
102
|
+
expect(result).toHaveLength(4);
|
|
103
|
+
const post = result.find((p) => p.slug === "novo-post-ia");
|
|
104
|
+
expect(post.title).toBe("Novo post gerado por IA");
|
|
105
|
+
expect(post.source).toBe("remote");
|
|
106
|
+
});
|
|
107
|
+
it("dedupe 'local-wins' — mantém o local, ignora o remoto", () => {
|
|
108
|
+
const localCom = [
|
|
109
|
+
...localFixture,
|
|
110
|
+
{ ...localFixture[0], slug: "novo-post-ia", title: "Versão local sagrada" },
|
|
111
|
+
];
|
|
112
|
+
const result = mergePosts({
|
|
113
|
+
local: localCom,
|
|
114
|
+
remote: remoteFixture,
|
|
115
|
+
mapLocal,
|
|
116
|
+
dedupe: "local-wins",
|
|
117
|
+
});
|
|
118
|
+
expect(result).toHaveLength(4);
|
|
119
|
+
const post = result.find((p) => p.slug === "novo-post-ia");
|
|
120
|
+
expect(post.title).toBe("Versão local sagrada");
|
|
121
|
+
expect(post.source).toBe("local");
|
|
122
|
+
});
|
|
123
|
+
it("dedupe 'throw' — lança MergeConflictError com slug do conflito", () => {
|
|
124
|
+
const localCom = [
|
|
125
|
+
...localFixture,
|
|
126
|
+
{ ...localFixture[0], slug: "novo-post-ia", title: "Conflito" },
|
|
127
|
+
];
|
|
128
|
+
expect(() => mergePosts({
|
|
129
|
+
local: localCom,
|
|
130
|
+
remote: remoteFixture,
|
|
131
|
+
mapLocal,
|
|
132
|
+
dedupe: "throw",
|
|
133
|
+
})).toThrowError(MergeConflictError);
|
|
134
|
+
});
|
|
135
|
+
it("mapLocal retornando null filtra o post (cliente quer esconder rascunhos)", () => {
|
|
136
|
+
const result = mergePosts({
|
|
137
|
+
local: localFixture,
|
|
138
|
+
remote: remoteFixture,
|
|
139
|
+
mapLocal: (p) => (p.slug === "post-antigo-2024" ? null : mapLocal(p)),
|
|
140
|
+
});
|
|
141
|
+
expect(result.find((p) => p.slug === "post-antigo-2024")).toBeUndefined();
|
|
142
|
+
expect(result).toHaveLength(3);
|
|
143
|
+
});
|
|
144
|
+
it("filtra remotos sem slug/title/publishedAt (defesa contra API inconsistente)", () => {
|
|
145
|
+
const remoteCom = [
|
|
146
|
+
...remoteFixture,
|
|
147
|
+
// sem slug
|
|
148
|
+
{ id: "x", slug: null, title: "x", featuredImageUrl: null, words: 0, publishedAt: "2026-01-01" },
|
|
149
|
+
// sem title
|
|
150
|
+
{ id: "y", slug: "y", title: null, featuredImageUrl: null, words: 0, publishedAt: "2026-01-01" },
|
|
151
|
+
// sem publishedAt
|
|
152
|
+
{ id: "z", slug: "z", title: "z", featuredImageUrl: null, words: 0, publishedAt: null },
|
|
153
|
+
];
|
|
154
|
+
const result = mergePosts({ local: [], remote: remoteCom, mapLocal });
|
|
155
|
+
expect(result).toHaveLength(2); // só os 2 válidos do fixture
|
|
156
|
+
});
|
|
157
|
+
it("lista vazia em ambos os lados retorna []", () => {
|
|
158
|
+
const result = mergePosts({ local: [], remote: [], mapLocal });
|
|
159
|
+
expect(result).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
it("só locais (cliente sem painel ainda) retorna locais ordenados", () => {
|
|
162
|
+
const result = mergePosts({ local: localFixture, remote: [], mapLocal });
|
|
163
|
+
expect(result).toHaveLength(2);
|
|
164
|
+
expect(result[0].source).toBe("local");
|
|
165
|
+
});
|
|
166
|
+
it("só remotos (cliente sem posts antigos) retorna remotos ordenados", () => {
|
|
167
|
+
const result = mergePosts({ local: [], remote: remoteFixture, mapLocal });
|
|
168
|
+
expect(result).toHaveLength(2);
|
|
169
|
+
expect(result[0].source).toBe("remote");
|
|
170
|
+
});
|
|
171
|
+
it("ArticleListItem em modo 'plain' (sem metaDescription) vira excerpt null", () => {
|
|
172
|
+
const remotePlain = [
|
|
173
|
+
{
|
|
174
|
+
id: "uuid-3",
|
|
175
|
+
slug: "post-cancelled",
|
|
176
|
+
title: "Post de site cancelado",
|
|
177
|
+
featuredImageUrl: null,
|
|
178
|
+
words: 500,
|
|
179
|
+
publishedAt: "2026-05-01T10:00:00Z",
|
|
180
|
+
// sem metaTitle/metaDescription (plain mode)
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
const result = mergePosts({ local: [], remote: remotePlain, mapLocal });
|
|
184
|
+
expect(result).toHaveLength(1);
|
|
185
|
+
expect(result[0].excerpt).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
it("data inválida vai pro final do feed sem quebrar", () => {
|
|
188
|
+
const remoteInvalido = [
|
|
189
|
+
{
|
|
190
|
+
id: "good",
|
|
191
|
+
slug: "good",
|
|
192
|
+
title: "Good",
|
|
193
|
+
featuredImageUrl: null,
|
|
194
|
+
words: 100,
|
|
195
|
+
publishedAt: "2026-05-20T10:00:00Z",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "bad",
|
|
199
|
+
slug: "bad",
|
|
200
|
+
title: "Bad",
|
|
201
|
+
featuredImageUrl: null,
|
|
202
|
+
words: 100,
|
|
203
|
+
publishedAt: "not-a-date",
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
const result = mergePosts({ local: [], remote: remoteInvalido, mapLocal });
|
|
207
|
+
expect(result).toHaveLength(2);
|
|
208
|
+
expect(result[0].slug).toBe("good"); // válido primeiro
|
|
209
|
+
expect(result[1].slug).toBe("bad"); // inválido vai pro final
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe("UnifiedPost type", () => {
|
|
213
|
+
it("source flag permite filtrar por origem na UI", () => {
|
|
214
|
+
const result = mergePosts({
|
|
215
|
+
local: localFixture,
|
|
216
|
+
remote: remoteFixture,
|
|
217
|
+
mapLocal,
|
|
218
|
+
});
|
|
219
|
+
const onlyRemote = result.filter((p) => p.source === "remote");
|
|
220
|
+
expect(onlyRemote).toHaveLength(2);
|
|
221
|
+
onlyRemote.forEach((p) => expect(p.source).toBe("remote"));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
//# sourceMappingURL=merge.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.test.js","sourceRoot":"","sources":["../src/merge.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED