@rxdrag/website-lib-core 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rxdrag/website-lib-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.ts"
@@ -23,9 +23,9 @@
23
23
  "@types/react-dom": "^19.1.0",
24
24
  "eslint": "^9.39.2",
25
25
  "typescript": "^5",
26
+ "@rxdrag/tiptap-preview": "0.1.0",
26
27
  "@rxdrag/eslint-config-custom": "0.3.0",
27
- "@rxdrag/tsconfig": "0.3.0",
28
- "@rxdrag/tiptap-preview": "0.1.0"
28
+ "@rxdrag/tsconfig": "0.3.0"
29
29
  },
30
30
  "dependencies": {
31
31
  "@iconify/utils": "^3.0.2",
@@ -33,8 +33,8 @@
33
33
  "gsap": "^3.12.7",
34
34
  "hls.js": "^1.6.13",
35
35
  "lodash-es": "^4.17.21",
36
- "@rxdrag/entify-lib": "0.1.0",
37
- "@rxdrag/rxcms-models": "0.4.0"
36
+ "@rxdrag/entify-lib": "0.1.1",
37
+ "@rxdrag/rxcms-models": "0.4.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "astro": "^5.18.1",
@@ -15,6 +15,9 @@ import {
15
15
  queryPosts,
16
16
  queryProductCategories,
17
17
  queryProducts,
18
+ queryProductsBatch,
19
+ queryPostsBatch,
20
+ PostsBatchOptions,
18
21
  queryTagCategories,
19
22
  queryTags,
20
23
  queryUserPosts,
@@ -298,6 +301,45 @@ export class Entify implements IEntify {
298
301
  )?.items;
299
302
  }
300
303
 
304
+ /**
305
+ * 批量获取多组产品列表(不同分类/分页),底层折叠为最少的 HTTP 请求,
306
+ * 用于静态构建时显著减少网络往返。返回顺序与入参顺序严格一致。
307
+ */
308
+ public async getProductsBatch(
309
+ conditionsList: ListConditions[],
310
+ imageSize?: ImageSize,
311
+ addonFields?: (keyof Product)[],
312
+ ) {
313
+ const langAbbr = this.ensureLangAbbr();
314
+ const size = imageSize ?? this.imageSizes.productThumbnial;
315
+ const results = await queryProductsBatch(
316
+ conditionsList,
317
+ size,
318
+ this.envVariables,
319
+ addonFields,
320
+ langAbbr,
321
+ );
322
+ return results.map((r) => r?.items);
323
+ }
324
+
325
+ /**
326
+ * 批量获取多组文章列表(不同分类/分页),底层折叠为最少的 HTTP 请求。
327
+ */
328
+ public async getPostsBatch(
329
+ conditionsList: ListConditions[],
330
+ options?: PostsBatchOptions,
331
+ ) {
332
+ const langAbbr = this.ensureLangAbbr();
333
+ const coverSize = options?.coverSize ?? this.imageSizes.postThumbnial;
334
+ const results = await queryPostsBatch(
335
+ conditionsList,
336
+ { coverSize, addonFields: options?.addonFields },
337
+ this.envVariables,
338
+ langAbbr,
339
+ );
340
+ return results.map((r) => r?.items);
341
+ }
342
+
301
343
  public async getProductListPaths(options: {
302
344
  category?: string;
303
345
  pageSize: number;
@@ -120,6 +120,19 @@ export interface IEntify {
120
120
  addonFields?: (keyof Product)[] | undefined
121
121
  ): Promise<TProduct[] | undefined>;
122
122
 
123
+ /** 批量获取多组产品列表(折叠为最少 HTTP 请求) */
124
+ getProductsBatch(
125
+ conditionsList: ListConditions[],
126
+ imageSize?: ImageSize,
127
+ addonFields?: (keyof Product)[]
128
+ ): Promise<(TProduct[] | undefined)[]>;
129
+
130
+ /** 批量获取多组文章列表(折叠为最少 HTTP 请求) */
131
+ getPostsBatch(
132
+ conditionsList: ListConditions[],
133
+ options?: { coverSize?: ImageSize; addonFields?: (keyof import("@rxdrag/rxcms-models").Post)[] }
134
+ ): Promise<(TPost[] | undefined)[]>;
135
+
123
136
  getProductListPaths(options: {
124
137
  category?: string;
125
138
  pageSize: number;
@@ -6,6 +6,9 @@ export * from "./newQueryPostOptions";
6
6
  export * from "./newQueryProductOptions";
7
7
  export * from "./newQueryProductsMediaOptions";
8
8
  export * from "./queryEntityList";
9
+ export * from "./queryEntityListBatch";
10
+ export * from "./queryProductsBatch";
11
+ export * from "./queryPostsBatch";
9
12
  export * from "./queryFeaturedProducts";
10
13
  export * from "./queryLangs";
11
14
  export * from "./queryLatestPosts";
@@ -0,0 +1,71 @@
1
+ import { IQueryOptions, ListResult } from "@rxdrag/entify-lib";
2
+ import { createEntifyClient } from "./createEntifyClient";
3
+ import { EnvVariables } from "../types";
4
+ import {
5
+ Post,
6
+ PostCategoryEntityName,
7
+ PostEntityName,
8
+ Product,
9
+ ProductCategoryEntityName,
10
+ ProductEntityName,
11
+ UserEntityName,
12
+ } from "@rxdrag/rxcms-models";
13
+ import {
14
+ postCategoriesToViewModel,
15
+ postListToViewModel,
16
+ productCategoriesToViewModel,
17
+ productListToViewModel,
18
+ userListToViewModel,
19
+ } from "../view-model";
20
+
21
+ /**
22
+ * queryEntityList 的批量版本:把 N 个 list 查询通过 GraphQL 别名打包成一次 HTTP 请求。
23
+ *
24
+ * 与 queryEntityList 行为对齐:
25
+ * - 当 envVariables.websiteId 存在时,统一向每个子查询注入 website.id 过滤
26
+ * - 按子查询的 entityName 走对应 viewmodel 转换
27
+ * - 返回顺序与入参顺序严格一致
28
+ */
29
+ export async function queryEntityListBatch<
30
+ T,
31
+ WhereExp = unknown,
32
+ OrderBy = unknown,
33
+ DistinctExp = unknown
34
+ >(
35
+ optionsList: IQueryOptions<T, WhereExp, OrderBy, DistinctExp>[],
36
+ envVariables: EnvVariables,
37
+ ) {
38
+ if (!optionsList?.length) return [];
39
+ const client = createEntifyClient(envVariables);
40
+
41
+ const scoped = optionsList.map((options) =>
42
+ !envVariables.websiteId
43
+ ? options
44
+ : options.setQueryArgs({
45
+ ...options.getQueryArgs(),
46
+ where: {
47
+ website: { id: { _eq: envVariables.websiteId } },
48
+ ...options.getQueryArgs()?.where,
49
+ } as WhereExp,
50
+ }),
51
+ );
52
+
53
+ const results = await client.batchEntityList<T, WhereExp, OrderBy, DistinctExp>(scoped);
54
+
55
+ return scoped.map((options, i) => {
56
+ const result = results[i];
57
+ switch (options.entityName) {
58
+ case ProductEntityName:
59
+ return productListToViewModel(result as ListResult<Product> | undefined);
60
+ case PostEntityName:
61
+ return postListToViewModel(result as ListResult<Post> | undefined);
62
+ case ProductCategoryEntityName:
63
+ return productCategoriesToViewModel(result as ListResult<Product> | undefined);
64
+ case PostCategoryEntityName:
65
+ return postCategoriesToViewModel(result as ListResult<Post> | undefined);
66
+ case UserEntityName:
67
+ return userListToViewModel(result as ListResult<Post> | undefined);
68
+ }
69
+ return result;
70
+ });
71
+ }
@@ -0,0 +1,187 @@
1
+ import {
2
+ Post,
3
+ PostBoolExp,
4
+ PostOrderBy,
5
+ PostDistinctExp,
6
+ PostFields,
7
+ PublishableStatus,
8
+ PostAssciations,
9
+ PostQueryOptions,
10
+ MediaQueryOptions,
11
+ UserQueryOptions,
12
+ UserFields,
13
+ ImageSize,
14
+ PostCategoryFields,
15
+ MediaFields,
16
+ PostCategory,
17
+ PostCategoryBoolExp,
18
+ PostCategoryOrderBy,
19
+ PostCategoryDistinctExp,
20
+ PostCategoryQueryOptions,
21
+ TagFields,
22
+ } from "@rxdrag/rxcms-models";
23
+ import { ListResult } from "@rxdrag/entify-lib";
24
+ import { ListConditions } from "./queryPosts";
25
+ import { queryEntityListBatch } from "./queryEntityListBatch";
26
+ import { createEntifyClient } from "./createEntifyClient";
27
+ import { EnvVariables } from "../types";
28
+ import { TPost } from "../view-model";
29
+ import { CategoryNode, collectCategoryIds } from "./collectCategoryIds";
30
+
31
+ /**
32
+ * 批量文章列表查询:把同一语言下多个分类/分页的 posts 查询折叠成最少的 HTTP 请求。
33
+ *
34
+ * 与 queryPosts 行为对齐:分类树展开(三级)、website 注入、viewmodel 转换、author/cover 字段。
35
+ */
36
+ export interface PostsBatchOptions {
37
+ coverSize?: ImageSize;
38
+ addonFields?: (keyof Post)[];
39
+ }
40
+
41
+ export async function queryPostsBatch(
42
+ conditionsList: ListConditions[],
43
+ options: PostsBatchOptions | undefined,
44
+ envVariables: EnvVariables,
45
+ langAbbr: string,
46
+ ): Promise<(ListResult<TPost> | undefined)[]> {
47
+ if (!conditionsList?.length) return [];
48
+
49
+ const categoryIdsBySlug = await resolvePostCategoryIds(
50
+ conditionsList.map((c) => c.category).filter((s): s is string => !!s),
51
+ envVariables,
52
+ langAbbr,
53
+ );
54
+
55
+ const postOptions = conditionsList.map((c) =>
56
+ buildPostQueryOptions(c, options, langAbbr, categoryIdsBySlug),
57
+ );
58
+
59
+ const results = await queryEntityListBatch<
60
+ Post,
61
+ PostBoolExp,
62
+ PostOrderBy,
63
+ PostDistinctExp
64
+ >(postOptions, envVariables);
65
+
66
+ return results.map((r) => r as ListResult<TPost> | undefined);
67
+ }
68
+
69
+ async function resolvePostCategoryIds(
70
+ slugs: string[],
71
+ envVariables: EnvVariables,
72
+ langAbbr: string,
73
+ ): Promise<Map<string, string[]>> {
74
+ const map = new Map<string, string[]>();
75
+ const uniq = Array.from(new Set(slugs));
76
+ if (!uniq.length) return map;
77
+
78
+ const client = createEntifyClient(envVariables);
79
+ const categoryOptions = uniq.map((slug) =>
80
+ new PostCategoryQueryOptions(
81
+ [PostCategoryFields.id, PostCategoryFields.slug],
82
+ {
83
+ where: {
84
+ slug: { _eq: slug },
85
+ lang: { abbr: { _eq: langAbbr } },
86
+ },
87
+ },
88
+ ).children(
89
+ new PostCategoryQueryOptions([
90
+ PostCategoryFields.id,
91
+ PostCategoryFields.slug,
92
+ ]).children([PostCategoryFields.id]),
93
+ ),
94
+ );
95
+
96
+ const results = await client.batchEntityList<
97
+ PostCategory,
98
+ PostCategoryBoolExp,
99
+ PostCategoryOrderBy,
100
+ PostCategoryDistinctExp
101
+ >(categoryOptions);
102
+
103
+ uniq.forEach((slug, i) => {
104
+ const item = results[i]?.items?.[0];
105
+ if (item) {
106
+ map.set(slug, collectCategoryIds(item as unknown as CategoryNode));
107
+ }
108
+ });
109
+ return map;
110
+ }
111
+
112
+ function buildPostQueryOptions(
113
+ conditions: ListConditions,
114
+ options: PostsBatchOptions | undefined,
115
+ langAbbr: string,
116
+ categoryIdsBySlug: Map<string, string[]>,
117
+ ): PostQueryOptions {
118
+ const { category: categorySlug, page = 1, pageSize } = conditions;
119
+ const addonFields = options?.addonFields;
120
+ const coverSize = options?.coverSize;
121
+
122
+ const where: Record<string, unknown> = {};
123
+ if (categorySlug) {
124
+ const ids = categoryIdsBySlug.get(categorySlug);
125
+ if (ids?.length) {
126
+ where[PostAssciations.category] = { id: { _in: ids } };
127
+ }
128
+ }
129
+
130
+ const defaultFields = [
131
+ PostFields.id,
132
+ PostFields.title,
133
+ PostFields.slug,
134
+ PostFields.description,
135
+ PostFields.status,
136
+ PostFields.updatedAt,
137
+ PostFields.createdAt,
138
+ PostFields.publishedAt,
139
+ ];
140
+ const fields = addonFields || defaultFields;
141
+
142
+ const queryOptions = new PostQueryOptions(fields, {
143
+ offset: (page - 1) * pageSize,
144
+ limit: pageSize,
145
+ where: {
146
+ [PostFields.status]: { _eq: PublishableStatus.published },
147
+ lang: { abbr: { _eq: langAbbr } },
148
+ _or: [
149
+ { [PostFields.isDeleted]: { _eq: false } },
150
+ { [PostFields.isDeleted]: { _isNull: true } },
151
+ ],
152
+ ...where,
153
+ },
154
+ orderBy: [
155
+ { [PostFields.pinToTop]: "desc" },
156
+ { [PostFields.seqValue]: "asc" },
157
+ { [PostFields.updatedAt]: "desc" },
158
+ ],
159
+ })
160
+ .category([PostCategoryFields.id, PostCategoryFields.name, PostCategoryFields.slug])
161
+ .tags([TagFields.id, TagFields.name, TagFields.color]);
162
+
163
+ if (!addonFields || addonFields.includes("cover" as keyof Post)) {
164
+ queryOptions.cover(
165
+ new MediaQueryOptions().file([
166
+ "thumbnail",
167
+ coverSize
168
+ ? `resize(width:${coverSize.width}, height:${coverSize.height})`
169
+ : "resize(width:480, height:180)",
170
+ ]),
171
+ );
172
+ }
173
+
174
+ if (!addonFields || addonFields.includes("author" as keyof Post)) {
175
+ queryOptions.author(
176
+ new UserQueryOptions([UserFields.id, UserFields.name]).avatar(
177
+ new MediaQueryOptions([
178
+ MediaFields.id,
179
+ MediaFields.mediaType,
180
+ MediaFields.mediaRef,
181
+ ]).file(["thumbnail", "original"]),
182
+ ),
183
+ );
184
+ }
185
+
186
+ return queryOptions;
187
+ }
@@ -58,6 +58,11 @@ export async function queryProducts(
58
58
  slug: {
59
59
  _eq: categorySlug,
60
60
  },
61
+ lang: {
62
+ abbr: {
63
+ _eq: langAbbr,
64
+ },
65
+ },
61
66
  },
62
67
  }
63
68
  //支持三级结构
@@ -0,0 +1,161 @@
1
+ import {
2
+ Product,
3
+ ProductBoolExp,
4
+ ProductOrderBy,
5
+ ProductDistinctExp,
6
+ ProductFields,
7
+ ProductQueryOptions,
8
+ PublishableStatus,
9
+ ProductAssciations,
10
+ ProductCategory,
11
+ ProductCategoryBoolExp,
12
+ ProductCategoryOrderBy,
13
+ ProductCategoryDistinctExp,
14
+ ProductCategoryFields,
15
+ ProductCategoryQueryOptions,
16
+ TagFields,
17
+ ImageSize,
18
+ } from "@rxdrag/rxcms-models";
19
+ import { ListResult } from "@rxdrag/entify-lib";
20
+ import { ListConditions } from "./queryPosts";
21
+ import { queryEntityListBatch } from "./queryEntityListBatch";
22
+ import { newQueryProductsMediaOptions } from "./newQueryProductsMediaOptions";
23
+ import { createEntifyClient } from "./createEntifyClient";
24
+ import { EnvVariables } from "../types";
25
+ import { TProduct } from "../view-model";
26
+ import { CategoryNode, collectCategoryIds } from "./collectCategoryIds";
27
+
28
+ /**
29
+ * 批量产品列表查询:把同一语言下多个分类/分页的 products 查询折叠成最少的 HTTP 请求。
30
+ *
31
+ * 流程:
32
+ * 1. 收集所有非空 category slug,一次批量解析分类树(最多 1 次 HTTP)。
33
+ * 2. 复用统一的产品查询字段配置,构造 N 个 ProductQueryOptions。
34
+ * 3. 一次批量发出(1 次 HTTP),按入参顺序返回结果。
35
+ *
36
+ * 与 queryProducts 行为对齐:分类树展开(三级)、website 注入、viewmodel 转换。
37
+ */
38
+ export async function queryProductsBatch(
39
+ conditionsList: ListConditions[],
40
+ imageSize: ImageSize | undefined,
41
+ envVariables: EnvVariables,
42
+ addonFields: (keyof Product)[] | undefined,
43
+ langAbbr: string,
44
+ ): Promise<(ListResult<TProduct> | undefined)[]> {
45
+ if (!conditionsList?.length) return [];
46
+
47
+ const categoryIdsBySlug = await resolveProductCategoryIds(
48
+ conditionsList.map((c) => c.category).filter((s): s is string => !!s),
49
+ envVariables,
50
+ langAbbr,
51
+ );
52
+
53
+ const productOptions = conditionsList.map((c) =>
54
+ buildProductQueryOptions(c, imageSize, addonFields, langAbbr, categoryIdsBySlug),
55
+ );
56
+
57
+ const results = await queryEntityListBatch<
58
+ Product,
59
+ ProductBoolExp,
60
+ ProductOrderBy,
61
+ ProductDistinctExp
62
+ >(productOptions, envVariables);
63
+
64
+ return results.map((r) => r as ListResult<TProduct> | undefined);
65
+ }
66
+
67
+ async function resolveProductCategoryIds(
68
+ slugs: string[],
69
+ envVariables: EnvVariables,
70
+ langAbbr: string,
71
+ ): Promise<Map<string, string[]>> {
72
+ const map = new Map<string, string[]>();
73
+ const uniq = Array.from(new Set(slugs));
74
+ if (!uniq.length) return map;
75
+
76
+ const client = createEntifyClient(envVariables);
77
+ const categoryOptions = uniq.map((slug) =>
78
+ new ProductCategoryQueryOptions(
79
+ [ProductCategoryFields.id, ProductCategoryFields.slug],
80
+ {
81
+ where: {
82
+ slug: { _eq: slug },
83
+ lang: { abbr: { _eq: langAbbr } },
84
+ },
85
+ },
86
+ ).children(
87
+ new ProductCategoryQueryOptions([
88
+ ProductCategoryFields.id,
89
+ ProductCategoryFields.slug,
90
+ ]).children([ProductCategoryFields.id]),
91
+ ),
92
+ );
93
+
94
+ const results = await client.batchEntityList<
95
+ ProductCategory,
96
+ ProductCategoryBoolExp,
97
+ ProductCategoryOrderBy,
98
+ ProductCategoryDistinctExp
99
+ >(categoryOptions);
100
+
101
+ uniq.forEach((slug, i) => {
102
+ const item = results[i]?.items?.[0];
103
+ if (item) {
104
+ map.set(slug, collectCategoryIds(item as unknown as CategoryNode));
105
+ }
106
+ });
107
+ return map;
108
+ }
109
+
110
+ function buildProductQueryOptions(
111
+ conditions: ListConditions,
112
+ imageSize: ImageSize | undefined,
113
+ addonFields: (keyof Product)[] | undefined,
114
+ langAbbr: string,
115
+ categoryIdsBySlug: Map<string, string[]>,
116
+ ): ProductQueryOptions {
117
+ const { category: categorySlug, page = 1, pageSize = 10, orderBy } = conditions;
118
+
119
+ const where: Record<string, unknown> = {};
120
+ if (categorySlug) {
121
+ const ids = categoryIdsBySlug.get(categorySlug);
122
+ if (ids?.length) {
123
+ where[ProductAssciations.category] = { id: { _in: ids } };
124
+ }
125
+ }
126
+
127
+ const defaultFields = [
128
+ ProductFields.id,
129
+ ProductFields.slug,
130
+ ProductFields.title,
131
+ ProductFields.shortTitle,
132
+ ProductFields.description,
133
+ ProductFields.seqValue,
134
+ ProductFields.updatedAt,
135
+ ];
136
+ const fields = [...defaultFields, ...(addonFields || [])];
137
+
138
+ const queryOptions = new ProductQueryOptions(fields, {
139
+ offset: (page - 1) * pageSize,
140
+ limit: pageSize,
141
+ where: {
142
+ [ProductFields.status]: { _eq: PublishableStatus.published },
143
+ lang: { abbr: { _eq: langAbbr } },
144
+ _or: [
145
+ { [ProductFields.isDeleted]: { _eq: false } },
146
+ { [ProductFields.isDeleted]: { _isNull: true } },
147
+ ],
148
+ ...where,
149
+ },
150
+ orderBy: orderBy || [
151
+ { [ProductFields.seqValue]: "asc" },
152
+ { [ProductFields.updatedAt]: "desc" },
153
+ ],
154
+ }).tags([TagFields.id, TagFields.name, TagFields.color]);
155
+
156
+ if (!addonFields || !addonFields.includes("slug" as keyof Product)) {
157
+ queryOptions.mediaPivots(newQueryProductsMediaOptions(imageSize));
158
+ }
159
+
160
+ return queryOptions;
161
+ }