@grapadigital/shared-schemas 1.0.220 → 1.0.221
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/schemas/action.schema.d.ts +2 -1
- package/dist/schemas/action.schema.js +2 -2
- package/dist/schemas/content.schema.d.ts +6 -0
- package/dist/schemas/content.schema.js +16 -0
- package/dist/utils/normalize-content-link.util.d.ts +12 -0
- package/dist/utils/normalize-content-link.util.js +33 -0
- package/docs/code-review.md +27 -0
- package/docs/constitution.md +75 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/schemas/action.schema.ts +4 -3
- package/src/schemas/content.schema.ts +21 -0
- package/src/utils/normalize-content-link.util.ts +31 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -57,3 +57,4 @@ __exportStar(require("./schemas/export-comments-job.schema"), exports);
|
|
|
57
57
|
__exportStar(require("./schemas/job.schema"), exports);
|
|
58
58
|
__exportStar(require("./schemas/snapshot.schema"), exports);
|
|
59
59
|
__exportStar(require("./schemas/lead.schema"), exports);
|
|
60
|
+
__exportStar(require("./utils/normalize-content-link.util"), exports);
|
|
@@ -91,7 +91,8 @@ export declare class Action {
|
|
|
91
91
|
nps?: number;
|
|
92
92
|
status: ActionStatus;
|
|
93
93
|
partnership: 'partner' | 'exclusive';
|
|
94
|
-
|
|
94
|
+
/** organic = Orgânicos do mês: post aprovado numa Ação de produto sai dela. Ver GD-699. */
|
|
95
|
+
type: 'seeding' | 'ugc' | 'creator' | 'organic';
|
|
95
96
|
socialnetworks?: string[];
|
|
96
97
|
recruitedInfluencers?: number;
|
|
97
98
|
hiredInfluencers?: number;
|
|
@@ -170,8 +170,8 @@ __decorate([
|
|
|
170
170
|
__metadata("design:type", String)
|
|
171
171
|
], Action.prototype, "partnership", void 0);
|
|
172
172
|
__decorate([
|
|
173
|
-
(0, swagger_1.ApiProperty)({ enum: ['seeding', 'ugc', 'creator'], example: 'seeding' }),
|
|
174
|
-
(0, mongoose_1.Prop)({ type: String, enum: ['seeding', 'ugc', 'creator'], required: true }),
|
|
173
|
+
(0, swagger_1.ApiProperty)({ enum: ['seeding', 'ugc', 'creator', 'organic'], example: 'seeding' }),
|
|
174
|
+
(0, mongoose_1.Prop)({ type: String, enum: ['seeding', 'ugc', 'creator', 'organic'], required: true }),
|
|
175
175
|
__metadata("design:type", String)
|
|
176
176
|
], Action.prototype, "type", void 0);
|
|
177
177
|
__decorate([
|
|
@@ -222,6 +222,12 @@ export declare class Content {
|
|
|
222
222
|
/** _id de uma Action.phases. Ver GD-691. */
|
|
223
223
|
phase?: Types.ObjectId | null;
|
|
224
224
|
phaseSource?: ContentClassificationSource | null;
|
|
225
|
+
/**
|
|
226
|
+
* Link normalizado (normalizeContentLink) para achar o mesmo post em outras
|
|
227
|
+
* Ações. Preenchido pelo hook de save; quem troca o link por updateOne grava
|
|
228
|
+
* junto. Ver GD-699.
|
|
229
|
+
*/
|
|
230
|
+
linkKey?: string | null;
|
|
225
231
|
}
|
|
226
232
|
export declare const ContentSchema: MongooseSchema<Content, import("mongoose").Model<Content, any, any, any, import("mongoose").Document<unknown, any, Content, any, {}> & Content & Required<{
|
|
227
233
|
_id: Types.ObjectId;
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.ContentModel = exports.ContentSchema = exports.Content = exports.ContentStatusChangedBySchema = exports.ContentStatusChangedBy = exports.CONTENT_CLASSIFICATION_SOURCE_VALUES = exports.CONTENT_METRICS_SOURCE_VALUES = exports.CONTENT_COMMENT_COUNT_SOURCE_VALUES = exports.CONTENT_REPORTABLE_STATUS_VALUES = exports.CONTENT_STATUS_VALUES = exports.EstimatesSchema = exports.Estimates = exports.ContentMetricsHistoryEntrySchema = exports.ContentMetricsHistoryEntry = exports.MetricsSchema = exports.Metrics = exports.SentimentAnalysisSchema = exports.SentimentAnalysis = exports.SentimentScoreSchema = exports.SentimentScore = exports.WordCloudSchema = exports.WordCloud = exports.SentimentCommentSchema = exports.SentimentComment = void 0;
|
|
13
13
|
const mongoose_1 = require("@nestjs/mongoose");
|
|
14
14
|
const mongoose_2 = require("mongoose");
|
|
15
|
+
const normalize_content_link_util_1 = require("../utils/normalize-content-link.util");
|
|
15
16
|
let SentimentComment = class SentimentComment {
|
|
16
17
|
};
|
|
17
18
|
__decorate([
|
|
@@ -471,11 +472,23 @@ __decorate([
|
|
|
471
472
|
(0, mongoose_1.Prop)({ type: String, enum: exports.CONTENT_CLASSIFICATION_SOURCE_VALUES, required: false, default: null }),
|
|
472
473
|
__metadata("design:type", Object)
|
|
473
474
|
], Content.prototype, "phaseSource", void 0);
|
|
475
|
+
__decorate([
|
|
476
|
+
(0, mongoose_1.Prop)({ type: String, required: false, default: null }),
|
|
477
|
+
__metadata("design:type", Object)
|
|
478
|
+
], Content.prototype, "linkKey", void 0);
|
|
474
479
|
Content = __decorate([
|
|
475
480
|
(0, mongoose_1.Schema)({ timestamps: true })
|
|
476
481
|
], Content);
|
|
477
482
|
exports.Content = Content;
|
|
478
483
|
exports.ContentSchema = mongoose_1.SchemaFactory.createForClass(Content);
|
|
484
|
+
// Mantém o linkKey em todo create/save (captura, create-content, scripts com o
|
|
485
|
+
// schema). updateOne/findOneAndUpdate não passam por aqui.
|
|
486
|
+
exports.ContentSchema.pre('save', function (next) {
|
|
487
|
+
if (this.isNew || this.isModified('link')) {
|
|
488
|
+
this.linkKey = (0, normalize_content_link_util_1.normalizeContentLink)(this.link);
|
|
489
|
+
}
|
|
490
|
+
next();
|
|
491
|
+
});
|
|
479
492
|
exports.ContentSchema.index({ action: 1, status: 1 });
|
|
480
493
|
exports.ContentSchema.index({ action: 1, profile: 1, type: 1 });
|
|
481
494
|
exports.ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
|
|
@@ -484,6 +497,9 @@ exports.ContentSchema.index({ profile: 1 });
|
|
|
484
497
|
exports.ContentSchema.index({ action: 1, phase: 1 });
|
|
485
498
|
exports.ContentSchema.index({ action: 1, isOrganic: 1 });
|
|
486
499
|
exports.ContentSchema.index({ createdAt: -1, status: 1 });
|
|
500
|
+
// Trava de aprovação entre Ações (GD-699): busca o mesmo post por publicação ou link.
|
|
501
|
+
exports.ContentSchema.index({ primetagId: 1 });
|
|
502
|
+
exports.ContentSchema.index({ linkKey: 1 });
|
|
487
503
|
// Chave de idempotência da captura da Primetag — conteúdos manuais (sem
|
|
488
504
|
// primetagId) ficam fora do índice.
|
|
489
505
|
exports.ContentSchema.index({ action: 1, primetagId: 1 }, { unique: true, partialFilterExpression: { primetagId: { $type: 'string' } } });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link do post sem protocolo, www./m., query, âncora e barra final. Mantém
|
|
3
|
+
* maiúsculas: o código do post (ex.: /reel/DCx8kQ2) diferencia. É a chave para
|
|
4
|
+
* achar o mesmo post em outras Ações (Content.linkKey). Ver GD-699.
|
|
5
|
+
*
|
|
6
|
+
* - YouTube `watch` mantém o `v` (o id do vídeo está na query);
|
|
7
|
+
* - link de perfil (um segmento só, ex.: instagram.com/usuario) e Drive: null;
|
|
8
|
+
* - null também para link vazio.
|
|
9
|
+
*
|
|
10
|
+
* A mesma regra está em scripts/lib/normalize-content-link.js: as duas mudam juntas.
|
|
11
|
+
*/
|
|
12
|
+
export declare const normalizeContentLink: (link?: string | null) => string | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeContentLink = void 0;
|
|
4
|
+
// Hosts cujo link não é de um post (ex.: pasta do Drive com os stories): não
|
|
5
|
+
// geram chave, senão posts diferentes casariam pelo mesmo link.
|
|
6
|
+
const NON_POST_HOSTS = ['drive.google.com', 'docs.google.com'];
|
|
7
|
+
/**
|
|
8
|
+
* Link do post sem protocolo, www./m., query, âncora e barra final. Mantém
|
|
9
|
+
* maiúsculas: o código do post (ex.: /reel/DCx8kQ2) diferencia. É a chave para
|
|
10
|
+
* achar o mesmo post em outras Ações (Content.linkKey). Ver GD-699.
|
|
11
|
+
*
|
|
12
|
+
* - YouTube `watch` mantém o `v` (o id do vídeo está na query);
|
|
13
|
+
* - link de perfil (um segmento só, ex.: instagram.com/usuario) e Drive: null;
|
|
14
|
+
* - null também para link vazio.
|
|
15
|
+
*
|
|
16
|
+
* A mesma regra está em scripts/lib/normalize-content-link.js: as duas mudam juntas.
|
|
17
|
+
*/
|
|
18
|
+
const normalizeContentLink = (link) => {
|
|
19
|
+
const raw = String(link ?? '')
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/^https?:\/\/(www\.|m\.)?/i, '');
|
|
22
|
+
const clean = raw.replace(/[?#].*$/, '').replace(/\/+$/, '');
|
|
23
|
+
const [host, ...segments] = clean.split('/');
|
|
24
|
+
if (!host || NON_POST_HOSTS.includes(host.toLowerCase()))
|
|
25
|
+
return null;
|
|
26
|
+
if (/^youtube\.com$/i.test(host) && segments[0] === 'watch') {
|
|
27
|
+
const videoId = raw.match(/[?&]v=([^&#]+)/)?.[1];
|
|
28
|
+
return videoId ? `${host}/watch?v=${videoId}` : null;
|
|
29
|
+
}
|
|
30
|
+
const minSegments = /^youtu\.be$/i.test(host) ? 1 : 2;
|
|
31
|
+
return segments.filter(Boolean).length >= minSegments ? clean : null;
|
|
32
|
+
};
|
|
33
|
+
exports.normalizeContentLink = normalizeContentLink;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Code Review: shared-schemas
|
|
2
|
+
|
|
3
|
+
Checklist de cada PR, junto da spec da Task e da `docs/constitution.md`. Usado pela skill `gd-code-review`.
|
|
4
|
+
|
|
5
|
+
## 1. Spec e commits
|
|
6
|
+
|
|
7
|
+
- [ ] A spec existe em `docs/specs/` (ou na API que consome) com `**Feature:** GD-xxx`?
|
|
8
|
+
- [ ] O schema bate com o contrato da spec?
|
|
9
|
+
- [ ] Cada commit segue a seção Commits da constitution? Para cada desvio, anotar como foi feito e como deveria ser.
|
|
10
|
+
|
|
11
|
+
## 2. Compatibilidade
|
|
12
|
+
|
|
13
|
+
- [ ] Campo novo é opcional no tipo e tem `default`? Documento antigo continua válido?
|
|
14
|
+
- [ ] Nenhum campo foi removido, renomeado ou mudou de tipo sem migração e sem a lista de repos afetados?
|
|
15
|
+
- [ ] Enum novo segue o padrão `*_VALUES as const` + tipo derivado e está no barrel?
|
|
16
|
+
- [ ] Índice novo faz sentido para as queries de quem consome?
|
|
17
|
+
|
|
18
|
+
## 3. Padrões
|
|
19
|
+
|
|
20
|
+
- [ ] `@ApiProperty` em todo campo exposto?
|
|
21
|
+
- [ ] JSDoc em português nos campos com regra?
|
|
22
|
+
- [ ] Sem helper de regra de negócio de uma API só?
|
|
23
|
+
|
|
24
|
+
## 4. Publicação
|
|
25
|
+
|
|
26
|
+
- [ ] A versão no `package.json` é a próxima livre no npm?
|
|
27
|
+
- [ ] `npm run build` passa?
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Constitution: shared-schemas
|
|
2
|
+
|
|
3
|
+
Regras do pacote `@grapadigital/shared-schemas`: os schemas Mongoose de todas as entidades, consumidos pelas APIs e pelos workers. Os fronts não dependem deste pacote (ele carrega mongoose e decorators do Nest); os tipos do front ficam no `@grapadigital/shared-app-modules`.
|
|
4
|
+
|
|
5
|
+
## Stack
|
|
6
|
+
|
|
7
|
+
- TypeScript compilado com `tsc` para CommonJS (`dist/`)
|
|
8
|
+
- Mongoose 8 + `@nestjs/mongoose` (`@Schema`, `@Prop`, `SchemaFactory`) e `@nestjs/swagger` (`@ApiProperty`)
|
|
9
|
+
- npm para publicar (`prepublishOnly` roda o build); sem lint nem testes configurados
|
|
10
|
+
- Prettier (`singleQuote: true`, `trailingComma: 'all'`)
|
|
11
|
+
|
|
12
|
+
## Estrutura
|
|
13
|
+
|
|
14
|
+
- Um arquivo por entidade em `src/schemas/<entidade>.schema.ts`, com a classe, o `<Entidade>Schema`, os índices e o model.
|
|
15
|
+
- Barrel em `src/index.ts`, com `export * from './schemas/<entidade>.schema'`. Nome que conflita entre arquivos é exportado nominalmente (ex.: `note.schema`).
|
|
16
|
+
- Subdocumento é uma classe com `@Schema({ _id: false })` no mesmo arquivo da entidade dona. Usar `@Schema()` (com `_id`) só quando outro documento aponta para ele (ex.: `ActionPhase`).
|
|
17
|
+
|
|
18
|
+
## Regras dos schemas
|
|
19
|
+
|
|
20
|
+
- **Enum** é uma constante `as const` exportada com o tipo derivado, usada no `@Prop` e no `@ApiProperty`:
|
|
21
|
+
```ts
|
|
22
|
+
export const ENTIDADE_STATUS_VALUES = ['a', 'b'] as const;
|
|
23
|
+
export type EntidadeStatus = (typeof ENTIDADE_STATUS_VALUES)[number];
|
|
24
|
+
```
|
|
25
|
+
- **Campo novo** é opcional no tipo (`campo?: T`) e tem `default` no `@Prop` (vazio ou `null`). Documento que já existe no banco não pode ficar inválido, e código que monta a entidade sem o campo não pode quebrar.
|
|
26
|
+
- **Todo campo exposto** tem `@ApiProperty`, para aparecer no Swagger das APIs.
|
|
27
|
+
- **JSDoc em português** em campo cuja regra não é óbvia: o que é, o que vazio significa e a Task (`Ver GD-xxx`).
|
|
28
|
+
- **Percentual** é gravado como fração (0,10 = 10%).
|
|
29
|
+
- **Nada de helper com regra de negócio** de uma API só: o pacote tem schemas, enums, tipos e constantes. Helper puro, sem I/O, usado por mais de um backend pode entrar.
|
|
30
|
+
- **Índice** novo é declarado no schema (`<Entidade>Schema.index(...)`); os serviços sobem com `autoIndex`.
|
|
31
|
+
|
|
32
|
+
## Versão e publicação
|
|
33
|
+
|
|
34
|
+
1. Subir a versão no `package.json` (conferir a última no npm: `npm view @grapadigital/shared-schemas version`).
|
|
35
|
+
2. `npm run build` e checar o `dist`.
|
|
36
|
+
3. Publicar (`npm publish`) e só então subir a dependência nas APIs e nos workers.
|
|
37
|
+
|
|
38
|
+
Mudança que quebra quem consome (remover ou renomear campo, mudar tipo) precisa de migração de dados e da lista de repos afetados na spec.
|
|
39
|
+
|
|
40
|
+
## Spec antes do código
|
|
41
|
+
|
|
42
|
+
Nenhuma implementação começa sem spec. A spec fica em `docs/specs/<slice>.md`, com `**Feature:** GD-xxx`, e segue o formato da seção 6 do `AGENTS.md` da raiz do workspace (fluxo `gd-spec-task` → `gd-execute-spec` → `gd-code-review`).
|
|
43
|
+
|
|
44
|
+
## Commits
|
|
45
|
+
|
|
46
|
+
Formato Conventional Commits: `<tipo>(<escopo>): <descrição>`.
|
|
47
|
+
|
|
48
|
+
| Tipo | Quando usar |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `feat` | Nova feature ou novo slice |
|
|
51
|
+
| `fix` | Correção de bug |
|
|
52
|
+
| `refactor` | Refatoração sem mudança de comportamento |
|
|
53
|
+
| `docs` | Documentação (`docs/`, `AGENTS.md`, `README`) |
|
|
54
|
+
| `test` | Testes |
|
|
55
|
+
| `chore` | Dependências, configs, scripts |
|
|
56
|
+
| `perf` | Performance |
|
|
57
|
+
|
|
58
|
+
- Descrição em inglês, no imperativo, em minúsculas, numa linha só de até 72 caracteres.
|
|
59
|
+
- O escopo é a entidade ou o módulo afetado (ex.: `feat(client): add create-client slice`).
|
|
60
|
+
- Um commit é uma mudança coesa. Commitar só os arquivos da Task.
|
|
61
|
+
- Sem trailer de co-autoria.
|
|
62
|
+
- Nunca commitar `console.log`, `debugger`, código comentado, `.env` ou segredo.
|
|
63
|
+
|
|
64
|
+
## Branch, PR e push
|
|
65
|
+
|
|
66
|
+
- Task de feature ou bug: branch a partir da `main` atualizada, no formato `<tipo>/GD-xxx-<descricao-curta>` (kebab-case, sem acento), e PR para a `main` com título e descrição em português e o link da Task.
|
|
67
|
+
- Antes do push:
|
|
68
|
+
|
|
69
|
+
- [ ] `npm run build` (tsc) compila sem erro
|
|
70
|
+
- [ ] campo novo é opcional ou tem default, e documentos antigos continuam válidos (`validateSync` com o `dist`)
|
|
71
|
+
- [ ] constante e tipo novos exportados pelo barrel (`src/index.ts`)
|
|
72
|
+
- [ ] versão nova no `package.json`, conferida no npm antes de publicar
|
|
73
|
+
- [ ] a spec existe em `docs/specs/` e bate com o que foi feito
|
|
74
|
+
- [ ] os commits seguem a seção Commits
|
|
75
|
+
- [ ] sem conflito com a `main`
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -149,9 +149,10 @@ export class Action {
|
|
|
149
149
|
@Prop({ type: String, enum: ['partner', 'exclusive'], required: true })
|
|
150
150
|
partnership: 'partner' | 'exclusive';
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
@
|
|
154
|
-
type: 'seeding'
|
|
152
|
+
/** organic = Orgânicos do mês: post aprovado numa Ação de produto sai dela. Ver GD-699. */
|
|
153
|
+
@ApiProperty({ enum: ['seeding', 'ugc', 'creator', 'organic'], example: 'seeding' })
|
|
154
|
+
@Prop({ type: String, enum: ['seeding', 'ugc', 'creator', 'organic'], required: true })
|
|
155
|
+
type: 'seeding' | 'ugc' | 'creator' | 'organic';
|
|
155
156
|
|
|
156
157
|
@ApiProperty({ example: ['instagram', 'tiktok'], required: false })
|
|
157
158
|
@Prop({ type: [String], required: false })
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from 'mongoose';
|
|
8
8
|
import { Profile } from './profile.schema';
|
|
9
9
|
import { Action } from './action.schema';
|
|
10
|
+
import { normalizeContentLink } from '../utils/normalize-content-link.util';
|
|
10
11
|
|
|
11
12
|
export type ContentDocument = HydratedDocument<Content>;
|
|
12
13
|
|
|
@@ -404,10 +405,27 @@ export class Content {
|
|
|
404
405
|
|
|
405
406
|
@Prop({ type: String, enum: CONTENT_CLASSIFICATION_SOURCE_VALUES, required: false, default: null })
|
|
406
407
|
phaseSource?: ContentClassificationSource | null;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Link normalizado (normalizeContentLink) para achar o mesmo post em outras
|
|
411
|
+
* Ações. Preenchido pelo hook de save; quem troca o link por updateOne grava
|
|
412
|
+
* junto. Ver GD-699.
|
|
413
|
+
*/
|
|
414
|
+
@Prop({ type: String, required: false, default: null })
|
|
415
|
+
linkKey?: string | null;
|
|
407
416
|
}
|
|
408
417
|
|
|
409
418
|
export const ContentSchema = SchemaFactory.createForClass(Content);
|
|
410
419
|
|
|
420
|
+
// Mantém o linkKey em todo create/save (captura, create-content, scripts com o
|
|
421
|
+
// schema). updateOne/findOneAndUpdate não passam por aqui.
|
|
422
|
+
ContentSchema.pre('save', function (next) {
|
|
423
|
+
if (this.isNew || this.isModified('link')) {
|
|
424
|
+
this.linkKey = normalizeContentLink(this.link);
|
|
425
|
+
}
|
|
426
|
+
next();
|
|
427
|
+
});
|
|
428
|
+
|
|
411
429
|
ContentSchema.index({ action: 1, status: 1 });
|
|
412
430
|
ContentSchema.index({ action: 1, profile: 1, type: 1 });
|
|
413
431
|
ContentSchema.index({ action: 1, profile: 1, socialnetwork: 1 });
|
|
@@ -416,6 +434,9 @@ ContentSchema.index({ profile: 1 });
|
|
|
416
434
|
ContentSchema.index({ action: 1, phase: 1 });
|
|
417
435
|
ContentSchema.index({ action: 1, isOrganic: 1 });
|
|
418
436
|
ContentSchema.index({ createdAt: -1, status: 1 });
|
|
437
|
+
// Trava de aprovação entre Ações (GD-699): busca o mesmo post por publicação ou link.
|
|
438
|
+
ContentSchema.index({ primetagId: 1 });
|
|
439
|
+
ContentSchema.index({ linkKey: 1 });
|
|
419
440
|
// Chave de idempotência da captura da Primetag — conteúdos manuais (sem
|
|
420
441
|
// primetagId) ficam fora do índice.
|
|
421
442
|
ContentSchema.index(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Hosts cujo link não é de um post (ex.: pasta do Drive com os stories): não
|
|
2
|
+
// geram chave, senão posts diferentes casariam pelo mesmo link.
|
|
3
|
+
const NON_POST_HOSTS = ['drive.google.com', 'docs.google.com'];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Link do post sem protocolo, www./m., query, âncora e barra final. Mantém
|
|
7
|
+
* maiúsculas: o código do post (ex.: /reel/DCx8kQ2) diferencia. É a chave para
|
|
8
|
+
* achar o mesmo post em outras Ações (Content.linkKey). Ver GD-699.
|
|
9
|
+
*
|
|
10
|
+
* - YouTube `watch` mantém o `v` (o id do vídeo está na query);
|
|
11
|
+
* - link de perfil (um segmento só, ex.: instagram.com/usuario) e Drive: null;
|
|
12
|
+
* - null também para link vazio.
|
|
13
|
+
*
|
|
14
|
+
* A mesma regra está em scripts/lib/normalize-content-link.js: as duas mudam juntas.
|
|
15
|
+
*/
|
|
16
|
+
export const normalizeContentLink = (link?: string | null): string | null => {
|
|
17
|
+
const raw = String(link ?? '')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/^https?:\/\/(www\.|m\.)?/i, '');
|
|
20
|
+
const clean = raw.replace(/[?#].*$/, '').replace(/\/+$/, '');
|
|
21
|
+
const [host, ...segments] = clean.split('/');
|
|
22
|
+
if (!host || NON_POST_HOSTS.includes(host.toLowerCase())) return null;
|
|
23
|
+
|
|
24
|
+
if (/^youtube\.com$/i.test(host) && segments[0] === 'watch') {
|
|
25
|
+
const videoId = raw.match(/[?&]v=([^&#]+)/)?.[1];
|
|
26
|
+
return videoId ? `${host}/watch?v=${videoId}` : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const minSegments = /^youtu\.be$/i.test(host) ? 1 : 2;
|
|
30
|
+
return segments.filter(Boolean).length >= minSegments ? clean : null;
|
|
31
|
+
};
|