@mamindom/contracts 1.0.141 → 1.0.142

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.
@@ -0,0 +1,157 @@
1
+ import { Observable } from "rxjs";
2
+ import { DeleteResponse, PaginationMeta, PaginationRequest } from "./common";
3
+ export declare const protobufPackage = "catalog.v1";
4
+ /**
5
+ * Частина combo. Однаковий формат і у DB (combo_parts JSON), і у gRPC.
6
+ * type: 'brand'|'price'|'color'|'size'
7
+ */
8
+ export interface FilterPart {
9
+ type: string;
10
+ /** Для brand/color/size: */
11
+ valueId?: string | undefined;
12
+ valueSlug?: string | undefined;
13
+ /** Для price: */
14
+ priceMin?: number | undefined;
15
+ priceMax?: number | undefined;
16
+ }
17
+ /** Локалізовані поля (uk/ru) у форматі { uk: "...", ru: "..." }. */
18
+ export interface LocaleStrings {
19
+ uk: string;
20
+ ru: string;
21
+ }
22
+ export interface SeoFilterResponse {
23
+ id: string;
24
+ categoryId: string;
25
+ comboHash: string;
26
+ comboParts: FilterPart[];
27
+ urlSegments: LocaleStrings | undefined;
28
+ title: LocaleStrings | undefined;
29
+ description: LocaleStrings | undefined;
30
+ h1: LocaleStrings | undefined;
31
+ isIndexed: boolean;
32
+ isAuto: boolean;
33
+ popularity: number;
34
+ createdAt: number;
35
+ updatedAt: number;
36
+ }
37
+ export interface ResolveSeoFilterRequest {
38
+ categoryId: string;
39
+ /**
40
+ * Канонічно відсортовані URL segments як приходять з Next.js, напр.
41
+ * ["brend-cybex", "cina-1000-5000"]. Caller гарантує що порядок canonical.
42
+ */
43
+ urlSegments: string[];
44
+ /** 'uk' | 'ru' */
45
+ locale: string;
46
+ }
47
+ export interface ResolveSeoFilterResponse {
48
+ /** Якщо false → caller рендерить категорію з noindex,follow. */
49
+ found: boolean;
50
+ /**
51
+ * Якщо false → caller додає <meta robots="noindex,follow"> навіть якщо
52
+ * запис знайдено (наприклад, isIndexed=false manual override).
53
+ */
54
+ isIndexed: boolean;
55
+ title: LocaleStrings | undefined;
56
+ description: LocaleStrings | undefined;
57
+ h1: LocaleStrings | undefined;
58
+ /** Canonical-URL-path без host: "/uk/c/koliaski/brend-cybex" */
59
+ canonicalPath: string;
60
+ /**
61
+ * Денормалізовані combo_parts з resolved valueId — для побудови
62
+ * запиту до products (categoryId + filter ids).
63
+ */
64
+ resolvedParts: FilterPart[];
65
+ }
66
+ export interface ListSeoFiltersRequest {
67
+ pagination: PaginationRequest | undefined;
68
+ categoryId?: string | undefined;
69
+ isIndexed?: boolean | undefined;
70
+ isAuto?: boolean | undefined;
71
+ /** Пошук за uk/ru title (для адмін-фільтра). */
72
+ search?: string | undefined;
73
+ }
74
+ export interface ListSeoFiltersResponse {
75
+ items: SeoFilterResponse[];
76
+ meta: PaginationMeta | undefined;
77
+ }
78
+ export interface GetSeoFilterRequest {
79
+ id: string;
80
+ }
81
+ export interface CreateSeoFilterRequest {
82
+ categoryId: string;
83
+ comboParts: FilterPart[];
84
+ title: LocaleStrings | undefined;
85
+ description: LocaleStrings | undefined;
86
+ h1: LocaleStrings | undefined;
87
+ isIndexed: boolean;
88
+ }
89
+ export interface UpdateSeoFilterRequest {
90
+ id: string;
91
+ title?: LocaleStrings | undefined;
92
+ description?: LocaleStrings | undefined;
93
+ h1?: LocaleStrings | undefined;
94
+ isIndexed?: boolean | undefined;
95
+ }
96
+ export interface DeleteSeoFilterRequest {
97
+ id: string;
98
+ }
99
+ export interface SuggestSeoFiltersRequest {
100
+ categoryId: string;
101
+ /** default 20 */
102
+ limit: number;
103
+ }
104
+ export interface SuggestSeoFilterCandidate {
105
+ /** Запропоновані combo_parts (валідні, з resolved slug). */
106
+ comboParts: FilterPart[];
107
+ /** Оцінка популярності: clicks / impressions / товарообіг (single число). */
108
+ score: number;
109
+ /** Чи існує вже SeoFilter з таким комбо (тоді UI ховає кнопку Create). */
110
+ alreadyExists: boolean;
111
+ /** Pre-built canonical URL path для preview. */
112
+ previewSegments: LocaleStrings | undefined;
113
+ }
114
+ export interface SuggestSeoFiltersResponse {
115
+ items: SuggestSeoFilterCandidate[];
116
+ }
117
+ export declare const CATALOG_V1_PACKAGE_NAME = "catalog.v1";
118
+ /** ─── Public ────────────────────────────────────────────────────────────── */
119
+ export interface SeoFilterServiceClient {
120
+ /**
121
+ * Резолв ЧПУ-URL → metadata + canonical. Викликається SSR Next.js для
122
+ * кожного category/[[...filters]] запиту. Якщо combo не знайдено —
123
+ * повертає isIndexed=false і автогенерований fallback metadata.
124
+ */
125
+ resolveSeoFilter(request: ResolveSeoFilterRequest): Observable<ResolveSeoFilterResponse>;
126
+ listSeoFilters(request: ListSeoFiltersRequest): Observable<ListSeoFiltersResponse>;
127
+ getSeoFilter(request: GetSeoFilterRequest): Observable<SeoFilterResponse>;
128
+ createSeoFilter(request: CreateSeoFilterRequest): Observable<SeoFilterResponse>;
129
+ updateSeoFilter(request: UpdateSeoFilterRequest): Observable<SeoFilterResponse>;
130
+ deleteSeoFilter(request: DeleteSeoFilterRequest): Observable<DeleteResponse>;
131
+ /**
132
+ * Топ-N непокритих combinations (за impressions/popularity) — для
133
+ * швидкого створення SeoFilter з адмін-форми "Автопідказки".
134
+ */
135
+ suggestSeoFilters(request: SuggestSeoFiltersRequest): Observable<SuggestSeoFiltersResponse>;
136
+ }
137
+ /** ─── Public ────────────────────────────────────────────────────────────── */
138
+ export interface SeoFilterServiceController {
139
+ /**
140
+ * Резолв ЧПУ-URL → metadata + canonical. Викликається SSR Next.js для
141
+ * кожного category/[[...filters]] запиту. Якщо combo не знайдено —
142
+ * повертає isIndexed=false і автогенерований fallback metadata.
143
+ */
144
+ resolveSeoFilter(request: ResolveSeoFilterRequest): Promise<ResolveSeoFilterResponse> | Observable<ResolveSeoFilterResponse> | ResolveSeoFilterResponse;
145
+ listSeoFilters(request: ListSeoFiltersRequest): Promise<ListSeoFiltersResponse> | Observable<ListSeoFiltersResponse> | ListSeoFiltersResponse;
146
+ getSeoFilter(request: GetSeoFilterRequest): Promise<SeoFilterResponse> | Observable<SeoFilterResponse> | SeoFilterResponse;
147
+ createSeoFilter(request: CreateSeoFilterRequest): Promise<SeoFilterResponse> | Observable<SeoFilterResponse> | SeoFilterResponse;
148
+ updateSeoFilter(request: UpdateSeoFilterRequest): Promise<SeoFilterResponse> | Observable<SeoFilterResponse> | SeoFilterResponse;
149
+ deleteSeoFilter(request: DeleteSeoFilterRequest): Promise<DeleteResponse> | Observable<DeleteResponse> | DeleteResponse;
150
+ /**
151
+ * Топ-N непокритих combinations (за impressions/popularity) — для
152
+ * швидкого створення SeoFilter з адмін-форми "Автопідказки".
153
+ */
154
+ suggestSeoFilters(request: SuggestSeoFiltersRequest): Promise<SuggestSeoFiltersResponse> | Observable<SuggestSeoFiltersResponse> | SuggestSeoFiltersResponse;
155
+ }
156
+ export declare function SeoFilterServiceControllerMethods(): (constructor: Function) => void;
157
+ export declare const SEO_FILTER_SERVICE_NAME = "SeoFilterService";
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
3
+ // versions:
4
+ // protoc-gen-ts_proto v2.11.4
5
+ // protoc v3.21.12
6
+ // source: seo_filter.proto
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.SEO_FILTER_SERVICE_NAME = exports.CATALOG_V1_PACKAGE_NAME = exports.protobufPackage = void 0;
9
+ exports.SeoFilterServiceControllerMethods = SeoFilterServiceControllerMethods;
10
+ /* eslint-disable */
11
+ const microservices_1 = require("@nestjs/microservices");
12
+ exports.protobufPackage = "catalog.v1";
13
+ exports.CATALOG_V1_PACKAGE_NAME = "catalog.v1";
14
+ function SeoFilterServiceControllerMethods() {
15
+ return function (constructor) {
16
+ const grpcMethods = [
17
+ "resolveSeoFilter",
18
+ "listSeoFilters",
19
+ "getSeoFilter",
20
+ "createSeoFilter",
21
+ "updateSeoFilter",
22
+ "deleteSeoFilter",
23
+ "suggestSeoFilters",
24
+ ];
25
+ for (const method of grpcMethods) {
26
+ const descriptor = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
27
+ (0, microservices_1.GrpcMethod)("SeoFilterService", method)(constructor.prototype[method], method, descriptor);
28
+ }
29
+ const grpcStreamMethods = [];
30
+ for (const method of grpcStreamMethods) {
31
+ const descriptor = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
32
+ (0, microservices_1.GrpcStreamMethod)("SeoFilterService", method)(constructor.prototype[method], method, descriptor);
33
+ }
34
+ };
35
+ }
36
+ exports.SEO_FILTER_SERVICE_NAME = "SeoFilterService";
@@ -0,0 +1,154 @@
1
+ syntax = "proto3";
2
+
3
+ package catalog.v1;
4
+
5
+ import "common.proto";
6
+
7
+
8
+ service SeoFilterService {
9
+ // ─── Public ──────────────────────────────────────────────────────────────
10
+
11
+ // Резолв ЧПУ-URL → metadata + canonical. Викликається SSR Next.js для
12
+ // кожного category/[[...filters]] запиту. Якщо combo не знайдено —
13
+ // повертає isIndexed=false і автогенерований fallback metadata.
14
+ rpc ResolveSeoFilter (ResolveSeoFilterRequest) returns (ResolveSeoFilterResponse);
15
+
16
+ // ─── Admin ───────────────────────────────────────────────────────────────
17
+
18
+ rpc ListSeoFilters (ListSeoFiltersRequest) returns (ListSeoFiltersResponse);
19
+ rpc GetSeoFilter (GetSeoFilterRequest) returns (SeoFilterResponse);
20
+ rpc CreateSeoFilter (CreateSeoFilterRequest) returns (SeoFilterResponse);
21
+ rpc UpdateSeoFilter (UpdateSeoFilterRequest) returns (SeoFilterResponse);
22
+ rpc DeleteSeoFilter (DeleteSeoFilterRequest) returns (DeleteResponse);
23
+
24
+ // Топ-N непокритих combinations (за impressions/popularity) — для
25
+ // швидкого створення SeoFilter з адмін-форми "Автопідказки".
26
+ rpc SuggestSeoFilters (SuggestSeoFiltersRequest) returns (SuggestSeoFiltersResponse);
27
+ }
28
+
29
+ // ─────────────────────────── Domain types ────────────────────────────────
30
+
31
+ // Частина combo. Однаковий формат і у DB (combo_parts JSON), і у gRPC.
32
+ // type: 'brand'|'price'|'color'|'size'
33
+ message FilterPart {
34
+ string type = 1;
35
+ // Для brand/color/size:
36
+ optional string value_id = 2;
37
+ optional string value_slug = 3;
38
+ // Для price:
39
+ optional int32 price_min = 4;
40
+ optional int32 price_max = 5;
41
+ }
42
+
43
+ // Локалізовані поля (uk/ru) у форматі { uk: "...", ru: "..." }.
44
+ message LocaleStrings {
45
+ string uk = 1;
46
+ string ru = 2;
47
+ }
48
+
49
+ message SeoFilterResponse {
50
+ string id = 1;
51
+ string category_id = 2;
52
+ string combo_hash = 3;
53
+ repeated FilterPart combo_parts = 4;
54
+ LocaleStrings url_segments = 5;
55
+ LocaleStrings title = 6;
56
+ LocaleStrings description = 7;
57
+ LocaleStrings h1 = 8;
58
+ bool is_indexed = 9;
59
+ bool is_auto = 10;
60
+ int32 popularity = 11;
61
+ int64 created_at = 12;
62
+ int64 updated_at = 13;
63
+ }
64
+
65
+ // ─────────────────────────── Resolve (public) ────────────────────────────
66
+
67
+ message ResolveSeoFilterRequest {
68
+ string category_id = 1;
69
+ // Канонічно відсортовані URL segments як приходять з Next.js, напр.
70
+ // ["brend-cybex", "cina-1000-5000"]. Caller гарантує що порядок canonical.
71
+ repeated string url_segments = 2;
72
+ string locale = 3; // 'uk' | 'ru'
73
+ }
74
+
75
+ message ResolveSeoFilterResponse {
76
+ // Якщо false → caller рендерить категорію з noindex,follow.
77
+ bool found = 1;
78
+ // Якщо false → caller додає <meta robots="noindex,follow"> навіть якщо
79
+ // запис знайдено (наприклад, isIndexed=false manual override).
80
+ bool is_indexed = 2;
81
+ LocaleStrings title = 3;
82
+ LocaleStrings description = 4;
83
+ LocaleStrings h1 = 5;
84
+ // Canonical-URL-path без host: "/uk/c/koliaski/brend-cybex"
85
+ string canonical_path = 6;
86
+ // Денормалізовані combo_parts з resolved valueId — для побудови
87
+ // запиту до products (categoryId + filter ids).
88
+ repeated FilterPart resolved_parts = 7;
89
+ }
90
+
91
+ // ─────────────────────────── List (admin) ────────────────────────────────
92
+
93
+ message ListSeoFiltersRequest {
94
+ PaginationRequest pagination = 1;
95
+ optional string category_id = 2;
96
+ optional bool is_indexed = 3;
97
+ optional bool is_auto = 4;
98
+ // Пошук за uk/ru title (для адмін-фільтра).
99
+ optional string search = 5;
100
+ }
101
+
102
+ message ListSeoFiltersResponse {
103
+ repeated SeoFilterResponse items = 1;
104
+ PaginationMeta meta = 2;
105
+ }
106
+
107
+ message GetSeoFilterRequest {
108
+ string id = 1;
109
+ }
110
+
111
+ // ─────────────────────────── Create / Update ─────────────────────────────
112
+
113
+ message CreateSeoFilterRequest {
114
+ string category_id = 1;
115
+ repeated FilterPart combo_parts = 2;
116
+ LocaleStrings title = 3;
117
+ LocaleStrings description = 4;
118
+ LocaleStrings h1 = 5;
119
+ bool is_indexed = 6;
120
+ }
121
+
122
+ message UpdateSeoFilterRequest {
123
+ string id = 1;
124
+ optional LocaleStrings title = 2;
125
+ optional LocaleStrings description = 3;
126
+ optional LocaleStrings h1 = 4;
127
+ optional bool is_indexed = 5;
128
+ }
129
+
130
+ message DeleteSeoFilterRequest {
131
+ string id = 1;
132
+ }
133
+
134
+ // ─────────────────────────── Suggestions (admin) ─────────────────────────
135
+
136
+ message SuggestSeoFiltersRequest {
137
+ string category_id = 1;
138
+ int32 limit = 2; // default 20
139
+ }
140
+
141
+ message SuggestSeoFilterCandidate {
142
+ // Запропоновані combo_parts (валідні, з resolved slug).
143
+ repeated FilterPart combo_parts = 1;
144
+ // Оцінка популярності: clicks / impressions / товарообіг (single число).
145
+ int32 score = 2;
146
+ // Чи існує вже SeoFilter з таким комбо (тоді UI ховає кнопку Create).
147
+ bool already_exists = 3;
148
+ // Pre-built canonical URL path для preview.
149
+ LocaleStrings preview_segments = 4;
150
+ }
151
+
152
+ message SuggestSeoFiltersResponse {
153
+ repeated SuggestSeoFilterCandidate items = 1;
154
+ }
@@ -16,6 +16,7 @@ export declare const PROTO_PATHS: {
16
16
  readonly BUNDLE: string;
17
17
  readonly FILTER: string;
18
18
  readonly SEARCH: string;
19
+ readonly SEO_FILTER: string;
19
20
  readonly PICKUP: string;
20
21
  readonly PRODUCT_REVIEW: string;
21
22
  readonly COMMON_PROMO: string;
@@ -20,6 +20,7 @@ exports.PROTO_PATHS = {
20
20
  BUNDLE: (0, node_path_1.join)(__dirname, '../../proto/bundle.proto'),
21
21
  FILTER: (0, node_path_1.join)(__dirname, '../../proto/shop_filters.proto'),
22
22
  SEARCH: (0, node_path_1.join)(__dirname, '../../proto/search.proto'),
23
+ SEO_FILTER: (0, node_path_1.join)(__dirname, '../../proto/seo_filter.proto'),
23
24
  PICKUP: (0, node_path_1.join)(__dirname, '../../proto/pickup.proto'),
24
25
  PRODUCT_REVIEW: (0, node_path_1.join)(__dirname, '../../proto/product_review.proto'),
25
26
  COMMON_PROMO: (0, node_path_1.join)(__dirname, '../../proto/common_promo.proto'),
@@ -0,0 +1,244 @@
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.11.4
4
+ // protoc v3.21.12
5
+ // source: seo_filter.proto
6
+
7
+ /* eslint-disable */
8
+ import { GrpcMethod, GrpcStreamMethod } from "@nestjs/microservices";
9
+ import { Observable } from "rxjs";
10
+ import { DeleteResponse, PaginationMeta, PaginationRequest } from "./common";
11
+
12
+ export const protobufPackage = "catalog.v1";
13
+
14
+ /**
15
+ * Частина combo. Однаковий формат і у DB (combo_parts JSON), і у gRPC.
16
+ * type: 'brand'|'price'|'color'|'size'
17
+ */
18
+ export interface FilterPart {
19
+ type: string;
20
+ /** Для brand/color/size: */
21
+ valueId?: string | undefined;
22
+ valueSlug?:
23
+ | string
24
+ | undefined;
25
+ /** Для price: */
26
+ priceMin?: number | undefined;
27
+ priceMax?: number | undefined;
28
+ }
29
+
30
+ /** Локалізовані поля (uk/ru) у форматі { uk: "...", ru: "..." }. */
31
+ export interface LocaleStrings {
32
+ uk: string;
33
+ ru: string;
34
+ }
35
+
36
+ export interface SeoFilterResponse {
37
+ id: string;
38
+ categoryId: string;
39
+ comboHash: string;
40
+ comboParts: FilterPart[];
41
+ urlSegments: LocaleStrings | undefined;
42
+ title: LocaleStrings | undefined;
43
+ description: LocaleStrings | undefined;
44
+ h1: LocaleStrings | undefined;
45
+ isIndexed: boolean;
46
+ isAuto: boolean;
47
+ popularity: number;
48
+ createdAt: number;
49
+ updatedAt: number;
50
+ }
51
+
52
+ export interface ResolveSeoFilterRequest {
53
+ categoryId: string;
54
+ /**
55
+ * Канонічно відсортовані URL segments як приходять з Next.js, напр.
56
+ * ["brend-cybex", "cina-1000-5000"]. Caller гарантує що порядок canonical.
57
+ */
58
+ urlSegments: string[];
59
+ /** 'uk' | 'ru' */
60
+ locale: string;
61
+ }
62
+
63
+ export interface ResolveSeoFilterResponse {
64
+ /** Якщо false → caller рендерить категорію з noindex,follow. */
65
+ found: boolean;
66
+ /**
67
+ * Якщо false → caller додає <meta robots="noindex,follow"> навіть якщо
68
+ * запис знайдено (наприклад, isIndexed=false manual override).
69
+ */
70
+ isIndexed: boolean;
71
+ title: LocaleStrings | undefined;
72
+ description: LocaleStrings | undefined;
73
+ h1:
74
+ | LocaleStrings
75
+ | undefined;
76
+ /** Canonical-URL-path без host: "/uk/c/koliaski/brend-cybex" */
77
+ canonicalPath: string;
78
+ /**
79
+ * Денормалізовані combo_parts з resolved valueId — для побудови
80
+ * запиту до products (categoryId + filter ids).
81
+ */
82
+ resolvedParts: FilterPart[];
83
+ }
84
+
85
+ export interface ListSeoFiltersRequest {
86
+ pagination: PaginationRequest | undefined;
87
+ categoryId?: string | undefined;
88
+ isIndexed?: boolean | undefined;
89
+ isAuto?:
90
+ | boolean
91
+ | undefined;
92
+ /** Пошук за uk/ru title (для адмін-фільтра). */
93
+ search?: string | undefined;
94
+ }
95
+
96
+ export interface ListSeoFiltersResponse {
97
+ items: SeoFilterResponse[];
98
+ meta: PaginationMeta | undefined;
99
+ }
100
+
101
+ export interface GetSeoFilterRequest {
102
+ id: string;
103
+ }
104
+
105
+ export interface CreateSeoFilterRequest {
106
+ categoryId: string;
107
+ comboParts: FilterPart[];
108
+ title: LocaleStrings | undefined;
109
+ description: LocaleStrings | undefined;
110
+ h1: LocaleStrings | undefined;
111
+ isIndexed: boolean;
112
+ }
113
+
114
+ export interface UpdateSeoFilterRequest {
115
+ id: string;
116
+ title?: LocaleStrings | undefined;
117
+ description?: LocaleStrings | undefined;
118
+ h1?: LocaleStrings | undefined;
119
+ isIndexed?: boolean | undefined;
120
+ }
121
+
122
+ export interface DeleteSeoFilterRequest {
123
+ id: string;
124
+ }
125
+
126
+ export interface SuggestSeoFiltersRequest {
127
+ categoryId: string;
128
+ /** default 20 */
129
+ limit: number;
130
+ }
131
+
132
+ export interface SuggestSeoFilterCandidate {
133
+ /** Запропоновані combo_parts (валідні, з resolved slug). */
134
+ comboParts: FilterPart[];
135
+ /** Оцінка популярності: clicks / impressions / товарообіг (single число). */
136
+ score: number;
137
+ /** Чи існує вже SeoFilter з таким комбо (тоді UI ховає кнопку Create). */
138
+ alreadyExists: boolean;
139
+ /** Pre-built canonical URL path для preview. */
140
+ previewSegments: LocaleStrings | undefined;
141
+ }
142
+
143
+ export interface SuggestSeoFiltersResponse {
144
+ items: SuggestSeoFilterCandidate[];
145
+ }
146
+
147
+ export const CATALOG_V1_PACKAGE_NAME = "catalog.v1";
148
+
149
+ /** ─── Public ────────────────────────────────────────────────────────────── */
150
+
151
+ export interface SeoFilterServiceClient {
152
+ /**
153
+ * Резолв ЧПУ-URL → metadata + canonical. Викликається SSR Next.js для
154
+ * кожного category/[[...filters]] запиту. Якщо combo не знайдено —
155
+ * повертає isIndexed=false і автогенерований fallback metadata.
156
+ */
157
+
158
+ resolveSeoFilter(request: ResolveSeoFilterRequest): Observable<ResolveSeoFilterResponse>;
159
+
160
+ listSeoFilters(request: ListSeoFiltersRequest): Observable<ListSeoFiltersResponse>;
161
+
162
+ getSeoFilter(request: GetSeoFilterRequest): Observable<SeoFilterResponse>;
163
+
164
+ createSeoFilter(request: CreateSeoFilterRequest): Observable<SeoFilterResponse>;
165
+
166
+ updateSeoFilter(request: UpdateSeoFilterRequest): Observable<SeoFilterResponse>;
167
+
168
+ deleteSeoFilter(request: DeleteSeoFilterRequest): Observable<DeleteResponse>;
169
+
170
+ /**
171
+ * Топ-N непокритих combinations (за impressions/popularity) — для
172
+ * швидкого створення SeoFilter з адмін-форми "Автопідказки".
173
+ */
174
+
175
+ suggestSeoFilters(request: SuggestSeoFiltersRequest): Observable<SuggestSeoFiltersResponse>;
176
+ }
177
+
178
+ /** ─── Public ────────────────────────────────────────────────────────────── */
179
+
180
+ export interface SeoFilterServiceController {
181
+ /**
182
+ * Резолв ЧПУ-URL → metadata + canonical. Викликається SSR Next.js для
183
+ * кожного category/[[...filters]] запиту. Якщо combo не знайдено —
184
+ * повертає isIndexed=false і автогенерований fallback metadata.
185
+ */
186
+
187
+ resolveSeoFilter(
188
+ request: ResolveSeoFilterRequest,
189
+ ): Promise<ResolveSeoFilterResponse> | Observable<ResolveSeoFilterResponse> | ResolveSeoFilterResponse;
190
+
191
+ listSeoFilters(
192
+ request: ListSeoFiltersRequest,
193
+ ): Promise<ListSeoFiltersResponse> | Observable<ListSeoFiltersResponse> | ListSeoFiltersResponse;
194
+
195
+ getSeoFilter(
196
+ request: GetSeoFilterRequest,
197
+ ): Promise<SeoFilterResponse> | Observable<SeoFilterResponse> | SeoFilterResponse;
198
+
199
+ createSeoFilter(
200
+ request: CreateSeoFilterRequest,
201
+ ): Promise<SeoFilterResponse> | Observable<SeoFilterResponse> | SeoFilterResponse;
202
+
203
+ updateSeoFilter(
204
+ request: UpdateSeoFilterRequest,
205
+ ): Promise<SeoFilterResponse> | Observable<SeoFilterResponse> | SeoFilterResponse;
206
+
207
+ deleteSeoFilter(
208
+ request: DeleteSeoFilterRequest,
209
+ ): Promise<DeleteResponse> | Observable<DeleteResponse> | DeleteResponse;
210
+
211
+ /**
212
+ * Топ-N непокритих combinations (за impressions/popularity) — для
213
+ * швидкого створення SeoFilter з адмін-форми "Автопідказки".
214
+ */
215
+
216
+ suggestSeoFilters(
217
+ request: SuggestSeoFiltersRequest,
218
+ ): Promise<SuggestSeoFiltersResponse> | Observable<SuggestSeoFiltersResponse> | SuggestSeoFiltersResponse;
219
+ }
220
+
221
+ export function SeoFilterServiceControllerMethods() {
222
+ return function (constructor: Function) {
223
+ const grpcMethods: string[] = [
224
+ "resolveSeoFilter",
225
+ "listSeoFilters",
226
+ "getSeoFilter",
227
+ "createSeoFilter",
228
+ "updateSeoFilter",
229
+ "deleteSeoFilter",
230
+ "suggestSeoFilters",
231
+ ];
232
+ for (const method of grpcMethods) {
233
+ const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
234
+ GrpcMethod("SeoFilterService", method)(constructor.prototype[method], method, descriptor);
235
+ }
236
+ const grpcStreamMethods: string[] = [];
237
+ for (const method of grpcStreamMethods) {
238
+ const descriptor: any = Reflect.getOwnPropertyDescriptor(constructor.prototype, method);
239
+ GrpcStreamMethod("SeoFilterService", method)(constructor.prototype[method], method, descriptor);
240
+ }
241
+ };
242
+ }
243
+
244
+ export const SEO_FILTER_SERVICE_NAME = "SeoFilterService";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mamindom/contracts",
3
3
  "description": "proto",
4
- "version": "1.0.141",
4
+ "version": "1.0.142",
5
5
  "main": "./dist/src/index.js",
6
6
  "types": "./dist/src/index.d.ts",
7
7
  "exports": {
@@ -0,0 +1,154 @@
1
+ syntax = "proto3";
2
+
3
+ package catalog.v1;
4
+
5
+ import "common.proto";
6
+
7
+
8
+ service SeoFilterService {
9
+ // ─── Public ──────────────────────────────────────────────────────────────
10
+
11
+ // Резолв ЧПУ-URL → metadata + canonical. Викликається SSR Next.js для
12
+ // кожного category/[[...filters]] запиту. Якщо combo не знайдено —
13
+ // повертає isIndexed=false і автогенерований fallback metadata.
14
+ rpc ResolveSeoFilter (ResolveSeoFilterRequest) returns (ResolveSeoFilterResponse);
15
+
16
+ // ─── Admin ───────────────────────────────────────────────────────────────
17
+
18
+ rpc ListSeoFilters (ListSeoFiltersRequest) returns (ListSeoFiltersResponse);
19
+ rpc GetSeoFilter (GetSeoFilterRequest) returns (SeoFilterResponse);
20
+ rpc CreateSeoFilter (CreateSeoFilterRequest) returns (SeoFilterResponse);
21
+ rpc UpdateSeoFilter (UpdateSeoFilterRequest) returns (SeoFilterResponse);
22
+ rpc DeleteSeoFilter (DeleteSeoFilterRequest) returns (DeleteResponse);
23
+
24
+ // Топ-N непокритих combinations (за impressions/popularity) — для
25
+ // швидкого створення SeoFilter з адмін-форми "Автопідказки".
26
+ rpc SuggestSeoFilters (SuggestSeoFiltersRequest) returns (SuggestSeoFiltersResponse);
27
+ }
28
+
29
+ // ─────────────────────────── Domain types ────────────────────────────────
30
+
31
+ // Частина combo. Однаковий формат і у DB (combo_parts JSON), і у gRPC.
32
+ // type: 'brand'|'price'|'color'|'size'
33
+ message FilterPart {
34
+ string type = 1;
35
+ // Для brand/color/size:
36
+ optional string value_id = 2;
37
+ optional string value_slug = 3;
38
+ // Для price:
39
+ optional int32 price_min = 4;
40
+ optional int32 price_max = 5;
41
+ }
42
+
43
+ // Локалізовані поля (uk/ru) у форматі { uk: "...", ru: "..." }.
44
+ message LocaleStrings {
45
+ string uk = 1;
46
+ string ru = 2;
47
+ }
48
+
49
+ message SeoFilterResponse {
50
+ string id = 1;
51
+ string category_id = 2;
52
+ string combo_hash = 3;
53
+ repeated FilterPart combo_parts = 4;
54
+ LocaleStrings url_segments = 5;
55
+ LocaleStrings title = 6;
56
+ LocaleStrings description = 7;
57
+ LocaleStrings h1 = 8;
58
+ bool is_indexed = 9;
59
+ bool is_auto = 10;
60
+ int32 popularity = 11;
61
+ int64 created_at = 12;
62
+ int64 updated_at = 13;
63
+ }
64
+
65
+ // ─────────────────────────── Resolve (public) ────────────────────────────
66
+
67
+ message ResolveSeoFilterRequest {
68
+ string category_id = 1;
69
+ // Канонічно відсортовані URL segments як приходять з Next.js, напр.
70
+ // ["brend-cybex", "cina-1000-5000"]. Caller гарантує що порядок canonical.
71
+ repeated string url_segments = 2;
72
+ string locale = 3; // 'uk' | 'ru'
73
+ }
74
+
75
+ message ResolveSeoFilterResponse {
76
+ // Якщо false → caller рендерить категорію з noindex,follow.
77
+ bool found = 1;
78
+ // Якщо false → caller додає <meta robots="noindex,follow"> навіть якщо
79
+ // запис знайдено (наприклад, isIndexed=false manual override).
80
+ bool is_indexed = 2;
81
+ LocaleStrings title = 3;
82
+ LocaleStrings description = 4;
83
+ LocaleStrings h1 = 5;
84
+ // Canonical-URL-path без host: "/uk/c/koliaski/brend-cybex"
85
+ string canonical_path = 6;
86
+ // Денормалізовані combo_parts з resolved valueId — для побудови
87
+ // запиту до products (categoryId + filter ids).
88
+ repeated FilterPart resolved_parts = 7;
89
+ }
90
+
91
+ // ─────────────────────────── List (admin) ────────────────────────────────
92
+
93
+ message ListSeoFiltersRequest {
94
+ PaginationRequest pagination = 1;
95
+ optional string category_id = 2;
96
+ optional bool is_indexed = 3;
97
+ optional bool is_auto = 4;
98
+ // Пошук за uk/ru title (для адмін-фільтра).
99
+ optional string search = 5;
100
+ }
101
+
102
+ message ListSeoFiltersResponse {
103
+ repeated SeoFilterResponse items = 1;
104
+ PaginationMeta meta = 2;
105
+ }
106
+
107
+ message GetSeoFilterRequest {
108
+ string id = 1;
109
+ }
110
+
111
+ // ─────────────────────────── Create / Update ─────────────────────────────
112
+
113
+ message CreateSeoFilterRequest {
114
+ string category_id = 1;
115
+ repeated FilterPart combo_parts = 2;
116
+ LocaleStrings title = 3;
117
+ LocaleStrings description = 4;
118
+ LocaleStrings h1 = 5;
119
+ bool is_indexed = 6;
120
+ }
121
+
122
+ message UpdateSeoFilterRequest {
123
+ string id = 1;
124
+ optional LocaleStrings title = 2;
125
+ optional LocaleStrings description = 3;
126
+ optional LocaleStrings h1 = 4;
127
+ optional bool is_indexed = 5;
128
+ }
129
+
130
+ message DeleteSeoFilterRequest {
131
+ string id = 1;
132
+ }
133
+
134
+ // ─────────────────────────── Suggestions (admin) ─────────────────────────
135
+
136
+ message SuggestSeoFiltersRequest {
137
+ string category_id = 1;
138
+ int32 limit = 2; // default 20
139
+ }
140
+
141
+ message SuggestSeoFilterCandidate {
142
+ // Запропоновані combo_parts (валідні, з resolved slug).
143
+ repeated FilterPart combo_parts = 1;
144
+ // Оцінка популярності: clicks / impressions / товарообіг (single число).
145
+ int32 score = 2;
146
+ // Чи існує вже SeoFilter з таким комбо (тоді UI ховає кнопку Create).
147
+ bool already_exists = 3;
148
+ // Pre-built canonical URL path для preview.
149
+ LocaleStrings preview_segments = 4;
150
+ }
151
+
152
+ message SuggestSeoFiltersResponse {
153
+ repeated SuggestSeoFilterCandidate items = 1;
154
+ }