-
-
Notifications
You must be signed in to change notification settings - Fork 949
/
slug-validator.ts
101 lines (96 loc) · 3.77 KB
/
slug-validator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { Injectable } from '@nestjs/common';
import { LanguageCode } from '@vendure/common/lib/generated-types';
import { normalizeString } from '@vendure/common/lib/normalize-string';
import { ID, Type } from '@vendure/common/lib/shared-types';
import { RequestContext } from '../../../api/common/request-context';
import { TransactionalConnection } from '../../../connection/transactional-connection';
import { Collection, Product } from '../../../entity';
import { VendureEntity } from '../../../entity/base/base.entity';
import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
/**
* @docsCategory service-helpers
* @docsPage SlugValidator
*/
export type InputWithSlug = {
id?: ID | null;
translations?: Array<{
id?: ID | null;
languageCode: LanguageCode;
slug?: string | null;
}> | null;
};
/**
* @docsCategory service-helpers
* @docsPage SlugValidator
*/
export type TranslationEntity = VendureEntity & {
id: ID;
languageCode: LanguageCode;
slug: string;
base: any;
};
/**
* @description
* Used to validate slugs to ensure they are URL-safe and unique. Designed to be used with translatable
* entities such as {@link Product} and {@link Collection}.
*
* @docsCategory service-helpers
* @docsWeight 0
*/
@Injectable()
export class SlugValidator {
constructor(private connection: TransactionalConnection) {}
/**
* Normalizes the slug to be URL-safe, and ensures it is unique for the given languageCode.
* Mutates the input.
*/
async validateSlugs<T extends InputWithSlug, E extends TranslationEntity>(
ctx: RequestContext,
input: T,
translationEntity: Type<E>,
): Promise<T> {
if (input.translations) {
for (const t of input.translations) {
if (t.slug) {
t.slug = normalizeString(t.slug, '-');
let match: E | null;
let suffix = 1;
const seen: ID[] = [];
const alreadySuffixed = /-\d+$/;
do {
const qb = this.connection
.getRepository(ctx, translationEntity)
.createQueryBuilder('translation')
.innerJoinAndSelect('translation.base', 'base')
.innerJoinAndSelect('base.channels', 'channel')
.where('channel.id = :channelId', { channelId: ctx.channelId })
.andWhere('translation.slug = :slug', { slug: t.slug })
.andWhere('translation.languageCode = :languageCode', {
languageCode: t.languageCode,
});
if (input.id) {
qb.andWhere('translation.base != :id', { id: input.id });
}
if (seen.length) {
qb.andWhere('translation.id NOT IN (:...seen)', { seen });
}
match = await qb.getOne();
if (match) {
if (!match.base.deletedAt) {
suffix++;
if (alreadySuffixed.test(t.slug)) {
t.slug = t.slug.replace(alreadySuffixed, `-${suffix}`);
} else {
t.slug = `${t.slug}-${suffix}`;
}
} else {
seen.push(match.id);
}
}
} while (match);
}
}
}
return input;
}
}