@rytass/cms-base-nestjs-graphql-module 0.1.11 → 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 +50 -90
- package/constants/option-providers.js +1 -1
- package/data-loaders/article.dataloader.js +28 -29
- package/data-loaders/members.dataloader.js +20 -21
- package/decorators/language.decorator.js +1 -1
- package/index.cjs.js +50 -52
- package/mutations/article.mutations.d.ts +1 -1
- package/mutations/article.mutations.js +2 -2
- package/package.json +5 -5
- package/queries/article.queries.js +1 -1
- package/queries/category.queries.js +1 -1
- package/resolvers/article-signature.resolver.js +1 -1
- package/resolvers/backstage-article.resolver.js +1 -1
- package/typings/cms-graphql-base-root-module-async-options.dto.d.ts +3 -2
- package/typings/custom-field-value.type.d.ts +5 -0
- package/typings/dto/resolved-create-article-args.dto.d.ts +24 -0
- package/typings/dto/resolved-create-category-args.dto.d.ts +13 -0
- package/typings/module-factory.type.d.ts +11 -0
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:
|
|
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:
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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:
|
|
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(
|
|
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 {
|
|
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 (
|
|
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(
|
|
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 (
|
|
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(
|
|
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,
|
|
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(
|
|
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.
|
|
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.
|
|
30
|
-
"@rytass/member-base-nestjs-module": "^0.2.
|
|
31
|
-
"dataloader": "^2.2.
|
|
32
|
-
"lru-cache": "^11.0
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
|
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:
|
|
6
|
-
inject?:
|
|
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,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[];
|