@rytass/cms-base-nestjs-graphql-module 0.1.10 → 0.1.12

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 CHANGED
@@ -5,6 +5,7 @@ Comprehensive GraphQL API layer for the CMS Base NestJS Module, providing a comp
5
5
  ## Features
6
6
 
7
7
  ### Core GraphQL API
8
+
8
9
  - [x] Complete GraphQL schema for articles and categories
9
10
  - [x] Public and backstage (admin) API endpoints
10
11
  - [x] Multi-language content delivery
@@ -12,6 +13,7 @@ Comprehensive GraphQL API layer for the CMS Base NestJS Module, providing a comp
12
13
  - [x] Paginated collections with metadata
13
14
 
14
15
  ### Advanced Content Management
16
+
15
17
  - [x] Article approval workflow resolvers
16
18
  - [x] Version control and draft management
17
19
  - [x] Category hierarchical relationships
@@ -19,6 +21,7 @@ Comprehensive GraphQL API layer for the CMS Base NestJS Module, providing a comp
19
21
  - [x] Full-text search integration
20
22
 
21
23
  ### Performance Optimization
24
+
22
25
  - [x] DataLoader pattern for N+1 query prevention
23
26
  - [x] Member data caching with LRU cache
24
27
  - [x] Article relationship optimization
@@ -26,6 +29,7 @@ Comprehensive GraphQL API layer for the CMS Base NestJS Module, providing a comp
26
29
  - [x] Query complexity analysis
27
30
 
28
31
  ### Integration Features
32
+
29
33
  - [x] Member system integration (@rytass/member-base-nestjs-module)
30
34
  - [x] Permission-based access control
31
35
  - [x] Quadrats rich content editor support
@@ -79,11 +83,11 @@ import { CMSBaseGraphQLModule } from '@rytass/cms-base-nestjs-graphql-module';
79
83
  signatureLevels: [
80
84
  { id: 1, name: 'Editor', level: 1 },
81
85
  { id: 2, name: 'Senior Editor', level: 2 },
82
- { id: 3, name: 'Chief Editor', level: 3 }
86
+ { id: 3, name: 'Chief Editor', level: 3 },
83
87
  ],
84
88
  fullTextSearchMode: true,
85
- autoReleaseAfterApproved: false
86
- })
89
+ autoReleaseAfterApproved: false,
90
+ }),
87
91
  ],
88
92
  })
89
93
  export class AppModule {}
@@ -107,9 +111,9 @@ import { CMSBaseGraphQLModule } from '@rytass/cms-base-nestjs-graphql-module';
107
111
  draftMode: configService.get('CMS_DRAFT_MODE') === 'true',
108
112
  signatureLevels: JSON.parse(configService.get('CMS_SIGNATURE_LEVELS') || '[]'),
109
113
  fullTextSearchMode: configService.get('CMS_FULL_TEXT_SEARCH') === 'true',
110
- autoReleaseAfterApproved: configService.get('CMS_AUTO_RELEASE') === 'true'
111
- })
112
- })
114
+ autoReleaseAfterApproved: configService.get('CMS_AUTO_RELEASE') === 'true',
115
+ }),
116
+ }),
113
117
  ],
114
118
  })
115
119
  export class AppModule {}
@@ -189,19 +193,8 @@ function ArticlePage({ articleId }: { articleId: string }) {
189
193
  #### Query Article Collection
190
194
 
191
195
  ```graphql
192
- query GetArticles(
193
- $page: Int
194
- $limit: Int
195
- $categoryId: ID
196
- $search: String
197
- $language: String
198
- ) {
199
- articles(
200
- page: $page
201
- limit: $limit
202
- categoryId: $categoryId
203
- search: $search
204
- ) {
196
+ query GetArticles($page: Int, $limit: Int, $categoryId: ID, $search: String, $language: String) {
197
+ articles(page: $page, limit: $limit, categoryId: $categoryId, search: $search) {
205
198
  items {
206
199
  articleId
207
200
  title
@@ -257,8 +250,8 @@ function ArticleList() {
257
250
  {data?.articles.items.map((article) => (
258
251
  <ArticleCard key={article.articleId} article={article} />
259
252
  ))}
260
-
261
- <Pagination
253
+
254
+ <Pagination
262
255
  currentPage={data?.articles.meta.currentPage}
263
256
  hasNextPage={data?.articles.meta.hasNextPage}
264
257
  onPageChange={setPage}
@@ -273,18 +266,8 @@ function ArticleList() {
273
266
  #### Query Articles with Management Data
274
267
 
275
268
  ```graphql
276
- query GetBackstageArticles(
277
- $page: Int
278
- $limit: Int
279
- $stage: String
280
- $authorId: ID
281
- ) {
282
- backstageArticles(
283
- page: $page
284
- limit: $limit
285
- stage: $stage
286
- authorId: $authorId
287
- ) {
269
+ query GetBackstageArticles($page: Int, $limit: Int, $stage: String, $authorId: ID) {
270
+ backstageArticles(page: $page, limit: $limit, stage: $stage, authorId: $authorId) {
288
271
  items {
289
272
  articleId
290
273
  title
@@ -386,7 +369,7 @@ function CreateArticleForm() {
386
369
  }
387
370
  }
388
371
  });
389
-
372
+
390
373
  console.log('Article created:', data.createArticle);
391
374
  } catch (err) {
392
375
  console.error('Error creating article:', err);
@@ -418,11 +401,7 @@ mutation UpdateArticle($id: ID!, $input: UpdateArticleInput!) {
418
401
 
419
402
  ```graphql
420
403
  mutation ApproveArticle($articleId: ID!, $level: Int!, $comments: String) {
421
- approveArticle(
422
- articleId: $articleId
423
- level: $level
424
- comments: $comments
425
- ) {
404
+ approveArticle(articleId: $articleId, level: $level, comments: $comments) {
426
405
  id
427
406
  approved
428
407
  approvedAt
@@ -441,17 +420,13 @@ mutation ApproveArticle($articleId: ID!, $level: Int!, $comments: String) {
441
420
  import { createParamDecorator, ExecutionContext } from '@nestjs/common';
442
421
  import { GqlExecutionContext } from '@nestjs/graphql';
443
422
 
444
- export const Language = createParamDecorator(
445
- (data: unknown, context: ExecutionContext) => {
446
- const ctx = GqlExecutionContext.create(context);
447
- const request = ctx.getContext().req;
448
-
449
- // Extract language from headers, query params, or JWT token
450
- return request.headers['accept-language'] ||
451
- request.query.language ||
452
- 'en-US';
453
- }
454
- );
423
+ export const Language = createParamDecorator((data: unknown, context: ExecutionContext) => {
424
+ const ctx = GqlExecutionContext.create(context);
425
+ const request = ctx.getContext().req;
426
+
427
+ // Extract language from headers, query params, or JWT token
428
+ return request.headers['accept-language'] || request.query.language || 'en-US';
429
+ });
455
430
  ```
456
431
 
457
432
  ### Multi-Language Queries
@@ -479,7 +454,7 @@ query GetMultiLanguageArticle($id: ID!) {
479
454
  // Frontend language switching
480
455
  function ArticleWithLanguage({ articleId }: { articleId: string }) {
481
456
  const [language, setLanguage] = useState('en-US');
482
-
457
+
483
458
  const { data } = useQuery(GET_ARTICLE, {
484
459
  variables: { id: articleId },
485
460
  context: {
@@ -512,7 +487,6 @@ import { ArticleDto } from '@rytass/cms-base-nestjs-graphql-module';
512
487
 
513
488
  @Resolver(() => ArticleDto)
514
489
  export class CustomArticleResolver {
515
-
516
490
  @ResolveField(() => String, { nullable: true })
517
491
  async seoTitle(@Parent() article: ArticleDto): Promise<string | null> {
518
492
  // Custom SEO title logic
@@ -555,22 +529,17 @@ import { Repository, In } from 'typeorm';
555
529
 
556
530
  @Injectable()
557
531
  export class CustomDataLoaderService {
558
-
559
532
  // Article view count loader
560
- public readonly articleViewsLoader = new DataLoader<string, number>(
561
- async (articleIds: string[]) => {
562
- const viewCounts = await this.getArticleViewCounts(articleIds);
563
- return articleIds.map(id => viewCounts[id] || 0);
564
- }
565
- );
533
+ public readonly articleViewsLoader = new DataLoader<string, number>(async (articleIds: string[]) => {
534
+ const viewCounts = await this.getArticleViewCounts(articleIds);
535
+ return articleIds.map(id => viewCounts[id] || 0);
536
+ });
566
537
 
567
538
  // Related articles loader
568
- public readonly relatedArticlesLoader = new DataLoader<string, ArticleDto[]>(
569
- async (articleIds: string[]) => {
570
- const relatedArticles = await this.getRelatedArticles(articleIds);
571
- return articleIds.map(id => relatedArticles[id] || []);
572
- }
573
- );
539
+ public readonly relatedArticlesLoader = new DataLoader<string, ArticleDto[]>(async (articleIds: string[]) => {
540
+ const relatedArticles = await this.getRelatedArticles(articleIds);
541
+ return articleIds.map(id => relatedArticles[id] || []);
542
+ });
574
543
 
575
544
  private async getArticleViewCounts(articleIds: string[]): Promise<Record<string, number>> {
576
545
  // Implement batch view count fetching
@@ -587,18 +556,8 @@ export class CustomDataLoaderService {
587
556
  ### Search Integration
588
557
 
589
558
  ```graphql
590
- query SearchArticles(
591
- $query: String!
592
- $filters: SearchFiltersInput
593
- $page: Int
594
- $limit: Int
595
- ) {
596
- searchArticles(
597
- query: $query
598
- filters: $filters
599
- page: $page
600
- limit: $limit
601
- ) {
559
+ query SearchArticles($query: String!, $filters: SearchFiltersInput, $page: Int, $limit: Int) {
560
+ searchArticles(query: $query, filters: $filters, page: $page, limit: $limit) {
602
561
  items {
603
562
  articleId
604
563
  title
@@ -643,7 +602,6 @@ import { BaseResource } from './constants/enum/base-resource.enum';
643
602
 
644
603
  @Resolver()
645
604
  export class PermissionResolver {
646
-
647
605
  @Query(() => [BackstageArticleDto])
648
606
  @RequireActions([BaseAction.READ], BaseResource.ARTICLE)
649
607
  async getAllArticles(): Promise<BackstageArticleDto[]> {
@@ -668,11 +626,10 @@ import { Resolver, Query, Context } from '@nestjs/graphql';
668
626
 
669
627
  @Resolver()
670
628
  export class SecurityResolver {
671
-
672
629
  @Query(() => [ArticleDto])
673
630
  async getUserArticles(@Context() context: any): Promise<ArticleDto[]> {
674
631
  const userId = context.req.user?.id;
675
-
632
+
676
633
  if (!userId) {
677
634
  throw new UnauthorizedException('User not authenticated');
678
635
  }
@@ -750,7 +707,7 @@ export function ArticleList() {
750
707
  },
751
708
  updateQuery: (prev, { fetchMoreResult }) => {
752
709
  if (!fetchMoreResult) return prev;
753
-
710
+
754
711
  return {
755
712
  articles: {
756
713
  ...fetchMoreResult.articles,
@@ -842,7 +799,7 @@ export const complexityConfig = {
842
799
  validators: [createComplexityLimitRule(1000)],
843
800
  createError: (max: number, actual: number) => {
844
801
  return new Error(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`);
845
- }
802
+ },
846
803
  };
847
804
  ```
848
805
 
@@ -860,15 +817,14 @@ import { redisStore } from 'cache-manager-redis-store';
860
817
  host: 'localhost',
861
818
  port: 6379,
862
819
  ttl: 600, // 10 minutes
863
- })
864
- ]
820
+ }),
821
+ ],
865
822
  })
866
823
  export class CacheConfig {}
867
824
 
868
825
  // Cached resolver
869
826
  @Resolver()
870
827
  export class CachedArticleResolver {
871
-
872
828
  @Query(() => [ArticleDto])
873
829
  @UseInterceptors(CacheInterceptor)
874
830
  @CacheTTL(300) // 5 minutes
@@ -901,7 +857,7 @@ describe('Article Resolver (e2e)', () => {
901
857
  CMSBaseGraphQLModule.forRoot({
902
858
  multipleLanguageMode: false,
903
859
  draftMode: true,
904
- })
860
+ }),
905
861
  ],
906
862
  }).compile();
907
863
 
@@ -924,7 +880,7 @@ describe('Article Resolver (e2e)', () => {
924
880
  .post('/graphql')
925
881
  .send({ query })
926
882
  .expect(200)
927
- .expect((res) => {
883
+ .expect(res => {
928
884
  expect(res.body.data.article).toBeDefined();
929
885
  expect(res.body.data.article.articleId).toBe('test-id');
930
886
  });
@@ -944,7 +900,7 @@ describe('ArticleDataLoader', () => {
944
900
 
945
901
  beforeEach(async () => {
946
902
  const module = await Test.createTestingModule({
947
- providers: [ArticleDataLoader]
903
+ providers: [ArticleDataLoader],
948
904
  }).compile();
949
905
 
950
906
  dataLoader = module.get<ArticleDataLoader>(ArticleDataLoader);
@@ -953,7 +909,7 @@ describe('ArticleDataLoader', () => {
953
909
  it('should batch load categories', async () => {
954
910
  const articleIds = ['article1', 'article2'];
955
911
  const categories = await dataLoader.categoriesLoader.loadMany(
956
- articleIds.map(id => ({ articleId: id, language: 'en-US' }))
912
+ articleIds.map(id => ({ articleId: id, language: 'en-US' })),
957
913
  );
958
914
 
959
915
  expect(categories).toHaveLength(2);
@@ -965,24 +921,28 @@ describe('ArticleDataLoader', () => {
965
921
  ## Best Practices
966
922
 
967
923
  ### Schema Design
924
+
968
925
  - Use consistent naming conventions for all GraphQL types
969
926
  - Implement proper pagination for all collection queries
970
927
  - Design efficient DataLoader patterns to prevent N+1 queries
971
928
  - Use nullable fields appropriately to handle missing data
972
929
 
973
930
  ### Performance
931
+
974
932
  - Implement query complexity analysis to prevent expensive queries
975
933
  - Use DataLoader for all relationship queries
976
934
  - Cache frequently accessed data with appropriate TTL
977
935
  - Optimize database queries with proper indexing
978
936
 
979
937
  ### Security
938
+
980
939
  - Implement proper authentication and authorization
981
940
  - Validate all input parameters and payloads
982
941
  - Use rate limiting to prevent abuse
983
942
  - Sanitize user-generated content
984
943
 
985
944
  ### Development
945
+
986
946
  - Write comprehensive tests for all resolvers
987
947
  - Use TypeScript for type safety across the GraphQL schema
988
948
  - Implement proper error handling and logging
@@ -1013,4 +973,4 @@ GRAPHQL_INTROSPECTION=true
1013
973
 
1014
974
  ## License
1015
975
 
1016
- MIT
976
+ MIT
@@ -1,4 +1,4 @@
1
- import { MAP_ARTICLE_CUSTOM_FIELDS_TO_ENTITY_COLUMNS, CMS_BASE_GRAPHQL_MODULE_OPTIONS, MAP_CATEGORY_CUSTOM_FIELDS_TO_ENTITY_COLUMNS } from '../typings/cms-graphql-base-providers.js';
1
+ import { CMS_BASE_GRAPHQL_MODULE_OPTIONS, MAP_ARTICLE_CUSTOM_FIELDS_TO_ENTITY_COLUMNS, MAP_CATEGORY_CUSTOM_FIELDS_TO_ENTITY_COLUMNS } from '../typings/cms-graphql-base-providers.js';
2
2
 
3
3
  const OptionProviders = [
4
4
  {
@@ -22,36 +22,35 @@ class ArticleDataLoader {
22
22
  articleRepo;
23
23
  constructor(articleRepo){
24
24
  this.articleRepo = articleRepo;
25
- this.categoriesLoader = new DataLoader(async (queryArgs)=>{
26
- const qb = this.articleRepo.createQueryBuilder('articles');
27
- qb.leftJoinAndSelect('articles.categories', 'categories');
28
- qb.leftJoinAndSelect('categories.multiLanguageNames', 'multiLanguageNames');
29
- qb.andWhere('articles.id IN (:...ids)', {
30
- ids: queryArgs.map((arg)=>arg.articleId)
31
- });
32
- const articles = await qb.getMany();
33
- const categoryMap = articles.reduce((vars, article)=>article.categories.reduce((cVars, category)=>category.multiLanguageNames.reduce((mVars, multiLanguageName)=>({
34
- ...mVars,
35
- [`${article.id}:${multiLanguageName.language}`]: [
36
- ...mVars[`${article.id}:${multiLanguageName.language}`] || [],
37
- {
38
- ...category,
39
- ...multiLanguageName
40
- }
41
- ]
42
- }), cVars), vars), {});
43
- return queryArgs.map((arg)=>categoryMap[`${arg.articleId}:${arg.language ?? DEFAULT_LANGUAGE}`] ?? []);
44
- }, {
45
- cache: true,
46
- cacheMap: new LRUCache({
47
- ttl: 1000 * 60,
48
- ttlAutopurge: true,
49
- max: 1000
50
- }),
51
- cacheKeyFn: (queryArgs)=>`${queryArgs.articleId}:${queryArgs.language ?? DEFAULT_LANGUAGE}`
52
- });
53
25
  }
54
- categoriesLoader;
26
+ categoriesLoader = new DataLoader(async (queryArgs)=>{
27
+ const qb = this.articleRepo.createQueryBuilder('articles');
28
+ qb.leftJoinAndSelect('articles.categories', 'categories');
29
+ qb.leftJoinAndSelect('categories.multiLanguageNames', 'multiLanguageNames');
30
+ qb.andWhere('articles.id IN (:...ids)', {
31
+ ids: queryArgs.map((arg)=>arg.articleId)
32
+ });
33
+ const articles = await qb.getMany();
34
+ const categoryMap = articles.reduce((vars, article)=>article.categories.reduce((cVars, category)=>category.multiLanguageNames.reduce((mVars, multiLanguageName)=>({
35
+ ...mVars,
36
+ [`${article.id}:${multiLanguageName.language}`]: [
37
+ ...mVars[`${article.id}:${multiLanguageName.language}`] || [],
38
+ {
39
+ ...category,
40
+ ...multiLanguageName
41
+ }
42
+ ]
43
+ }), cVars), vars), {});
44
+ return queryArgs.map((arg)=>categoryMap[`${arg.articleId}:${arg.language ?? DEFAULT_LANGUAGE}`] ?? []);
45
+ }, {
46
+ cache: true,
47
+ cacheMap: new LRUCache({
48
+ ttl: 1000 * 60,
49
+ ttlAutopurge: true,
50
+ max: 1000
51
+ }),
52
+ cacheKeyFn: (queryArgs)=>`${queryArgs.articleId}:${queryArgs.language ?? DEFAULT_LANGUAGE}`
53
+ });
55
54
  }
56
55
  ArticleDataLoader = _ts_decorate([
57
56
  Injectable(),
@@ -22,28 +22,27 @@ class MemberDataLoader {
22
22
  memberRepo;
23
23
  constructor(memberRepo){
24
24
  this.memberRepo = memberRepo;
25
- this.loader = new DataLoader(async (ids)=>{
26
- const qb = this.memberRepo.createQueryBuilder('members');
27
- qb.withDeleted();
28
- qb.andWhere('members.id IN (:...ids)', {
29
- ids
30
- });
31
- const users = await qb.getMany();
32
- const userMap = new Map(users.map((user)=>[
33
- user.id,
34
- user
35
- ]));
36
- return ids.map((id)=>userMap.get(id) ?? null);
37
- }, {
38
- cache: true,
39
- cacheMap: new LRUCache({
40
- ttl: 1000 * 60 * 10,
41
- ttlAutopurge: true,
42
- max: 1000
43
- })
44
- });
45
25
  }
46
- loader;
26
+ loader = new DataLoader(async (ids)=>{
27
+ const qb = this.memberRepo.createQueryBuilder('members');
28
+ qb.withDeleted();
29
+ qb.andWhere('members.id IN (:...ids)', {
30
+ ids
31
+ });
32
+ const users = await qb.getMany();
33
+ const userMap = new Map(users.map((user)=>[
34
+ user.id,
35
+ user
36
+ ]));
37
+ return ids.map((id)=>userMap.get(id) ?? null);
38
+ }, {
39
+ cache: true,
40
+ cacheMap: new LRUCache({
41
+ ttl: 1000 * 60 * 10,
42
+ ttlAutopurge: true,
43
+ max: 1000
44
+ })
45
+ });
47
46
  }
48
47
  MemberDataLoader = _ts_decorate([
49
48
  Injectable(),
@@ -2,7 +2,7 @@ import { createParamDecorator } from '@nestjs/common';
2
2
  import { DEFAULT_LANGUAGE } from '@rytass/cms-base-nestjs-module';
3
3
 
4
4
  const LANGUAGE_HEADER_KEY = 'x-language';
5
- const Language = createParamDecorator(async (data, context)=>{
5
+ const Language = createParamDecorator(async (_data, context)=>{
6
6
  const contextType = context.getType();
7
7
  switch(contextType){
8
8
  case 'graphql':
package/index.cjs.js CHANGED
@@ -389,7 +389,7 @@ class ArticleMutations {
389
389
  userId: memberId
390
390
  });
391
391
  }
392
- async putBackArticle(memberId, id) {
392
+ async putBackArticle(_memberId, id) {
393
393
  return this.articleService.putBack(id);
394
394
  }
395
395
  async approveArticle(id, version) {
@@ -929,7 +929,7 @@ ArticlesArgs = _ts_decorate$m([
929
929
  ], ArticlesArgs);
930
930
 
931
931
  const LANGUAGE_HEADER_KEY = 'x-language';
932
- const Language = common.createParamDecorator(async (data, context)=>{
932
+ const Language = common.createParamDecorator(async (_data, context)=>{
933
933
  const contextType = context.getType();
934
934
  switch(contextType){
935
935
  case 'graphql':
@@ -1419,28 +1419,27 @@ class MemberDataLoader {
1419
1419
  memberRepo;
1420
1420
  constructor(memberRepo){
1421
1421
  this.memberRepo = memberRepo;
1422
- this.loader = new DataLoader(async (ids)=>{
1423
- const qb = this.memberRepo.createQueryBuilder('members');
1424
- qb.withDeleted();
1425
- qb.andWhere('members.id IN (:...ids)', {
1426
- ids
1427
- });
1428
- const users = await qb.getMany();
1429
- const userMap = new Map(users.map((user)=>[
1430
- user.id,
1431
- user
1432
- ]));
1433
- return ids.map((id)=>userMap.get(id) ?? null);
1434
- }, {
1435
- cache: true,
1436
- cacheMap: new lruCache.LRUCache({
1437
- ttl: 1000 * 60 * 10,
1438
- ttlAutopurge: true,
1439
- max: 1000
1440
- })
1441
- });
1442
1422
  }
1443
- loader;
1423
+ loader = new DataLoader(async (ids)=>{
1424
+ const qb = this.memberRepo.createQueryBuilder('members');
1425
+ qb.withDeleted();
1426
+ qb.andWhere('members.id IN (:...ids)', {
1427
+ ids
1428
+ });
1429
+ const users = await qb.getMany();
1430
+ const userMap = new Map(users.map((user)=>[
1431
+ user.id,
1432
+ user
1433
+ ]));
1434
+ return ids.map((id)=>userMap.get(id) ?? null);
1435
+ }, {
1436
+ cache: true,
1437
+ cacheMap: new lruCache.LRUCache({
1438
+ ttl: 1000 * 60 * 10,
1439
+ ttlAutopurge: true,
1440
+ max: 1000
1441
+ })
1442
+ });
1444
1443
  }
1445
1444
  MemberDataLoader = _ts_decorate$c([
1446
1445
  common.Injectable(),
@@ -1469,36 +1468,35 @@ class ArticleDataLoader {
1469
1468
  articleRepo;
1470
1469
  constructor(articleRepo){
1471
1470
  this.articleRepo = articleRepo;
1472
- this.categoriesLoader = new DataLoader(async (queryArgs)=>{
1473
- const qb = this.articleRepo.createQueryBuilder('articles');
1474
- qb.leftJoinAndSelect('articles.categories', 'categories');
1475
- qb.leftJoinAndSelect('categories.multiLanguageNames', 'multiLanguageNames');
1476
- qb.andWhere('articles.id IN (:...ids)', {
1477
- ids: queryArgs.map((arg)=>arg.articleId)
1478
- });
1479
- const articles = await qb.getMany();
1480
- const categoryMap = articles.reduce((vars, article)=>article.categories.reduce((cVars, category)=>category.multiLanguageNames.reduce((mVars, multiLanguageName)=>({
1481
- ...mVars,
1482
- [`${article.id}:${multiLanguageName.language}`]: [
1483
- ...mVars[`${article.id}:${multiLanguageName.language}`] || [],
1484
- {
1485
- ...category,
1486
- ...multiLanguageName
1487
- }
1488
- ]
1489
- }), cVars), vars), {});
1490
- return queryArgs.map((arg)=>categoryMap[`${arg.articleId}:${arg.language ?? cmsBaseNestjsModule.DEFAULT_LANGUAGE}`] ?? []);
1491
- }, {
1492
- cache: true,
1493
- cacheMap: new lruCache.LRUCache({
1494
- ttl: 1000 * 60,
1495
- ttlAutopurge: true,
1496
- max: 1000
1497
- }),
1498
- cacheKeyFn: (queryArgs)=>`${queryArgs.articleId}:${queryArgs.language ?? cmsBaseNestjsModule.DEFAULT_LANGUAGE}`
1499
- });
1500
1471
  }
1501
- categoriesLoader;
1472
+ categoriesLoader = new DataLoader(async (queryArgs)=>{
1473
+ const qb = this.articleRepo.createQueryBuilder('articles');
1474
+ qb.leftJoinAndSelect('articles.categories', 'categories');
1475
+ qb.leftJoinAndSelect('categories.multiLanguageNames', 'multiLanguageNames');
1476
+ qb.andWhere('articles.id IN (:...ids)', {
1477
+ ids: queryArgs.map((arg)=>arg.articleId)
1478
+ });
1479
+ const articles = await qb.getMany();
1480
+ const categoryMap = articles.reduce((vars, article)=>article.categories.reduce((cVars, category)=>category.multiLanguageNames.reduce((mVars, multiLanguageName)=>({
1481
+ ...mVars,
1482
+ [`${article.id}:${multiLanguageName.language}`]: [
1483
+ ...mVars[`${article.id}:${multiLanguageName.language}`] || [],
1484
+ {
1485
+ ...category,
1486
+ ...multiLanguageName
1487
+ }
1488
+ ]
1489
+ }), cVars), vars), {});
1490
+ return queryArgs.map((arg)=>categoryMap[`${arg.articleId}:${arg.language ?? cmsBaseNestjsModule.DEFAULT_LANGUAGE}`] ?? []);
1491
+ }, {
1492
+ cache: true,
1493
+ cacheMap: new lruCache.LRUCache({
1494
+ ttl: 1000 * 60,
1495
+ ttlAutopurge: true,
1496
+ max: 1000
1497
+ }),
1498
+ cacheKeyFn: (queryArgs)=>`${queryArgs.articleId}:${queryArgs.language ?? cmsBaseNestjsModule.DEFAULT_LANGUAGE}`
1499
+ });
1502
1500
  }
1503
1501
  ArticleDataLoader = _ts_decorate$b([
1504
1502
  common.Injectable(),
@@ -14,7 +14,7 @@ export declare class ArticleMutations {
14
14
  deleteArticle(id: string): Promise<boolean>;
15
15
  deleteArticleVersion(id: string, version: number): Promise<boolean>;
16
16
  submitArticle(memberId: string, id: string): Promise<BackstageArticleDto>;
17
- putBackArticle(memberId: string, id: string): Promise<BackstageArticleDto>;
17
+ putBackArticle(_memberId: string, id: string): Promise<BackstageArticleDto>;
18
18
  approveArticle(id: string, version?: number | null): Promise<BackstageArticleDto>;
19
19
  rejectArticle(id: string, reason?: string | null): Promise<BackstageArticleDto>;
20
20
  releaseArticle(userId: string, id: string, releasedAt: Date, version?: number | null): Promise<BackstageArticleDto>;
@@ -1,4 +1,4 @@
1
- import { MULTIPLE_LANGUAGE_MODE, ArticleBaseService, DEFAULT_LANGUAGE } from '@rytass/cms-base-nestjs-module';
1
+ import { MULTIPLE_LANGUAGE_MODE, DEFAULT_LANGUAGE, ArticleBaseService } from '@rytass/cms-base-nestjs-module';
2
2
  import { Mutation, Args, ID, Int, Resolver } from '@nestjs/graphql';
3
3
  import { BackstageArticleDto } from '../dto/backstage-article.dto.js';
4
4
  import { CreateArticleArgs } from '../dto/create-article.args.js';
@@ -92,7 +92,7 @@ class ArticleMutations {
92
92
  userId: memberId
93
93
  });
94
94
  }
95
- async putBackArticle(memberId, id) {
95
+ async putBackArticle(_memberId, id) {
96
96
  return this.articleService.putBack(id);
97
97
  }
98
98
  async approveArticle(id, version) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/cms-base-nestjs-graphql-module",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Rytass Content Management System NestJS Base GraphQL Module",
5
5
  "keywords": [
6
6
  "rytass",
@@ -26,10 +26,10 @@
26
26
  "typeorm": "*"
27
27
  },
28
28
  "dependencies": {
29
- "@rytass/cms-base-nestjs-module": "^0.2.3",
30
- "@rytass/member-base-nestjs-module": "^0.2.8",
31
- "dataloader": "^2.2.2",
32
- "lru-cache": "^11.0.2"
29
+ "@rytass/cms-base-nestjs-module": "^0.2.5",
30
+ "@rytass/member-base-nestjs-module": "^0.2.9",
31
+ "dataloader": "^2.2.3",
32
+ "lru-cache": "^11.1.0"
33
33
  },
34
34
  "main": "./index.cjs.js",
35
35
  "module": "./index.js",
@@ -1,5 +1,5 @@
1
1
  import { Query, Args, ID, Int, Resolver } from '@nestjs/graphql';
2
- import { MULTIPLE_LANGUAGE_MODE, ArticleBaseService, ArticleStage, DEFAULT_LANGUAGE } from '@rytass/cms-base-nestjs-module';
2
+ import { MULTIPLE_LANGUAGE_MODE, DEFAULT_LANGUAGE, ArticleStage, ArticleBaseService } from '@rytass/cms-base-nestjs-module';
3
3
  import { ArticlesArgs } from '../dto/articles.args.js';
4
4
  import { IsPublic, AllowActions } from '@rytass/member-base-nestjs-module';
5
5
  import { Language } from '../decorators/language.decorator.js';
@@ -1,5 +1,5 @@
1
1
  import { Query, Args, ID, Resolver } from '@nestjs/graphql';
2
- import { MULTIPLE_LANGUAGE_MODE, CategoryBaseService, DEFAULT_LANGUAGE } from '@rytass/cms-base-nestjs-module';
2
+ import { MULTIPLE_LANGUAGE_MODE, DEFAULT_LANGUAGE, CategoryBaseService } from '@rytass/cms-base-nestjs-module';
3
3
  import { CategoriesArgs } from '../dto/categories.args.js';
4
4
  import { IsPublic, AllowActions } from '@rytass/member-base-nestjs-module';
5
5
  import { CategoryDto } from '../dto/category.dto.js';
@@ -3,7 +3,7 @@ import { ArticleSignatureDto } from '../dto/article-signature.dto.js';
3
3
  import { UserDto } from '../dto/user.dto.js';
4
4
  import { Authenticated } from '@rytass/member-base-nestjs-module';
5
5
  import { MemberDataLoader } from '../data-loaders/members.dataloader.js';
6
- import { ArticleSignatureEntity, DEFAULT_SIGNATURE_LEVEL } from '@rytass/cms-base-nestjs-module';
6
+ import { DEFAULT_SIGNATURE_LEVEL, ArticleSignatureEntity } from '@rytass/cms-base-nestjs-module';
7
7
  import { ArticleSignatureStepDto } from '../dto/article-signature-step.dto.js';
8
8
 
9
9
  function _ts_decorate(decorators, target, key, desc) {
@@ -2,7 +2,7 @@ import { ResolveField, ID, Root, Resolver } from '@nestjs/graphql';
2
2
  import { MemberDataLoader } from '../data-loaders/members.dataloader.js';
3
3
  import { UserDto } from '../dto/user.dto.js';
4
4
  import { Authenticated } from '@rytass/member-base-nestjs-module';
5
- import { ArticleStage, MULTIPLE_LANGUAGE_MODE, ArticleVersionDataLoader, ArticleDataLoader as ArticleDataLoader$1, ArticleSignatureDataLoader, DEFAULT_LANGUAGE } from '@rytass/cms-base-nestjs-module';
5
+ import { ArticleStage, MULTIPLE_LANGUAGE_MODE, DEFAULT_LANGUAGE, ArticleVersionDataLoader, ArticleDataLoader as ArticleDataLoader$1, ArticleSignatureDataLoader } from '@rytass/cms-base-nestjs-module';
6
6
  import { CategoryDto } from '../dto/category.dto.js';
7
7
  import { ArticleDataLoader } from '../data-loaders/article.dataloader.js';
8
8
  import { Language } from '../decorators/language.decorator.js';
@@ -1,9 +1,10 @@
1
1
  import { ModuleMetadata, Type } from '@nestjs/common';
2
2
  import { CMSGraphqlBaseModuleOptionFactory } from './cms-graphql-base-root-module-option-factory';
3
3
  import { CMSGraphqlBaseModuleOptionsDto } from './cms-graphql-base-root-module-options.dto';
4
+ import { FactoryFunctionArgs, DependencyInjectionToken } from './module-factory.type';
4
5
  export interface CMSGraphqlBaseModuleAsyncOptionsDto extends Pick<ModuleMetadata, 'imports'> {
5
- useFactory?: (...args: any[]) => Promise<CMSGraphqlBaseModuleOptionsDto> | CMSGraphqlBaseModuleOptionsDto;
6
- inject?: any[];
6
+ useFactory?: (...args: FactoryFunctionArgs) => Promise<CMSGraphqlBaseModuleOptionsDto> | CMSGraphqlBaseModuleOptionsDto;
7
+ inject?: DependencyInjectionToken[];
7
8
  useClass?: Type<CMSGraphqlBaseModuleOptionFactory>;
8
9
  useExisting?: Type<CMSGraphqlBaseModuleOptionFactory>;
9
10
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Represents the possible types for custom field values
3
+ * Used to replace 'any' in index signatures for custom fields
4
+ */
5
+ export type CustomFieldValue = string | number | boolean | Date | object | null | undefined;
@@ -0,0 +1,24 @@
1
+ import { QuadratsElement } from '@quadrats/core';
2
+ import { CustomFieldValue } from '../custom-field-value.type';
3
+ type BaseResolvedCreateArticleArgsDto = {
4
+ categoryIds: string[];
5
+ tags: string[];
6
+ submitted?: boolean;
7
+ signatureLevel?: string | null;
8
+ releasedAt?: Date | null;
9
+ [key: string]: CustomFieldValue;
10
+ };
11
+ export type SingleLanguageResolvedCreateArticleArgsDto = BaseResolvedCreateArticleArgsDto & {
12
+ title: string;
13
+ content: QuadratsElement[];
14
+ description?: string;
15
+ };
16
+ export type MultiLanguageResolvedCreateArticleArgsDto = BaseResolvedCreateArticleArgsDto & {
17
+ multiLanguageContents: Record<string, {
18
+ title: string;
19
+ description?: string;
20
+ content: QuadratsElement[];
21
+ }>;
22
+ };
23
+ export type ResolvedCreateArticleArgsDto = SingleLanguageResolvedCreateArticleArgsDto | MultiLanguageResolvedCreateArticleArgsDto;
24
+ export {};
@@ -0,0 +1,13 @@
1
+ import { CustomFieldValue } from '../custom-field-value.type';
2
+ type BaseResolvedCreateCategoryArgsDto = {
3
+ parentIds?: string[] | null;
4
+ [key: string]: CustomFieldValue;
5
+ };
6
+ export type SingleLanguageResolvedCreateCategoryArgsDto = BaseResolvedCreateCategoryArgsDto & {
7
+ name: string;
8
+ };
9
+ export type MultiLanguageResolvedCreateCategoryArgsDto = BaseResolvedCreateCategoryArgsDto & {
10
+ multiLanguageNames: Record<string, string>;
11
+ };
12
+ export type ResolvedCreateCategoryArgsDto = SingleLanguageResolvedCreateCategoryArgsDto | MultiLanguageResolvedCreateCategoryArgsDto;
13
+ export {};
@@ -0,0 +1,11 @@
1
+ import { InjectionToken } from '@nestjs/common';
2
+ /**
3
+ * Represents possible dependency injection tokens
4
+ * Used to replace 'any[]' in NestJS module configuration
5
+ */
6
+ export type DependencyInjectionToken = InjectionToken | string | symbol | Function;
7
+ /**
8
+ * Represents factory function arguments
9
+ * Used to replace 'any[]' in useFactory functions
10
+ */
11
+ export type FactoryFunctionArgs = unknown[];