@rytass/cms-base-nestjs-graphql-module 0.1.9 → 0.1.11

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.
Files changed (2) hide show
  1. package/README.md +1015 -2
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,3 +1,1016 @@
1
- # CMS Base System GraphQL for NestJS Projects
1
+ # Rytass Utils - CMS Base NestJS GraphQL Module
2
2
 
3
- ## Features
3
+ Comprehensive GraphQL API layer for the CMS Base NestJS Module, providing a complete content management system with GraphQL queries, mutations, and resolvers. Features multi-language support, approval workflows, version control, and optimized DataLoader patterns for high-performance content delivery.
4
+
5
+ ## Features
6
+
7
+ ### Core GraphQL API
8
+ - [x] Complete GraphQL schema for articles and categories
9
+ - [x] Public and backstage (admin) API endpoints
10
+ - [x] Multi-language content delivery
11
+ - [x] Real-time content queries with filters
12
+ - [x] Paginated collections with metadata
13
+
14
+ ### Advanced Content Management
15
+ - [x] Article approval workflow resolvers
16
+ - [x] Version control and draft management
17
+ - [x] Category hierarchical relationships
18
+ - [x] Custom field support in GraphQL
19
+ - [x] Full-text search integration
20
+
21
+ ### Performance Optimization
22
+ - [x] DataLoader pattern for N+1 query prevention
23
+ - [x] Member data caching with LRU cache
24
+ - [x] Article relationship optimization
25
+ - [x] Efficient batch loading for categories
26
+ - [x] Query complexity analysis
27
+
28
+ ### Integration Features
29
+ - [x] Member system integration (@rytass/member-base-nestjs-module)
30
+ - [x] Permission-based access control
31
+ - [x] Quadrats rich content editor support
32
+ - [x] Multi-language decorator system
33
+ - [x] TypeORM entity relationships
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install @rytass/cms-base-nestjs-graphql-module @rytass/cms-base-nestjs-module
39
+ # Peer dependencies
40
+ npm install @nestjs/common @nestjs/typeorm @nestjs/graphql typeorm
41
+ # or
42
+ yarn add @rytass/cms-base-nestjs-graphql-module @rytass/cms-base-nestjs-module
43
+ ```
44
+
45
+ ## Basic Setup
46
+
47
+ ### Module Configuration
48
+
49
+ ```typescript
50
+ // app.module.ts
51
+ import { Module } from '@nestjs/common';
52
+ import { GraphQLModule } from '@nestjs/graphql';
53
+ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
54
+ import { TypeOrmModule } from '@nestjs/typeorm';
55
+ import { CMSBaseGraphQLModule } from '@rytass/cms-base-nestjs-graphql-module';
56
+
57
+ @Module({
58
+ imports: [
59
+ TypeOrmModule.forRoot({
60
+ type: 'postgres',
61
+ host: 'localhost',
62
+ port: 5432,
63
+ username: 'username',
64
+ password: 'password',
65
+ database: 'cms_database',
66
+ autoLoadEntities: true,
67
+ synchronize: true, // Development only
68
+ }),
69
+ GraphQLModule.forRoot<ApolloDriverConfig>({
70
+ driver: ApolloDriver,
71
+ autoSchemaFile: 'schema.gql',
72
+ sortSchema: true,
73
+ playground: true,
74
+ introspection: true,
75
+ }),
76
+ CMSBaseGraphQLModule.forRoot({
77
+ multipleLanguageMode: true,
78
+ draftMode: true,
79
+ signatureLevels: [
80
+ { id: 1, name: 'Editor', level: 1 },
81
+ { id: 2, name: 'Senior Editor', level: 2 },
82
+ { id: 3, name: 'Chief Editor', level: 3 }
83
+ ],
84
+ fullTextSearchMode: true,
85
+ autoReleaseAfterApproved: false
86
+ })
87
+ ],
88
+ })
89
+ export class AppModule {}
90
+ ```
91
+
92
+ ### Async Configuration
93
+
94
+ ```typescript
95
+ // app.module.ts
96
+ import { ConfigModule, ConfigService } from '@nestjs/config';
97
+ import { CMSBaseGraphQLModule } from '@rytass/cms-base-nestjs-graphql-module';
98
+
99
+ @Module({
100
+ imports: [
101
+ ConfigModule.forRoot(),
102
+ CMSBaseGraphQLModule.forRootAsync({
103
+ imports: [ConfigModule],
104
+ inject: [ConfigService],
105
+ useFactory: (configService: ConfigService) => ({
106
+ multipleLanguageMode: configService.get('CMS_MULTI_LANGUAGE') === 'true',
107
+ draftMode: configService.get('CMS_DRAFT_MODE') === 'true',
108
+ signatureLevels: JSON.parse(configService.get('CMS_SIGNATURE_LEVELS') || '[]'),
109
+ fullTextSearchMode: configService.get('CMS_FULL_TEXT_SEARCH') === 'true',
110
+ autoReleaseAfterApproved: configService.get('CMS_AUTO_RELEASE') === 'true'
111
+ })
112
+ })
113
+ ],
114
+ })
115
+ export class AppModule {}
116
+ ```
117
+
118
+ ## GraphQL API Usage
119
+
120
+ ### Public Queries
121
+
122
+ #### Query Single Article
123
+
124
+ ```graphql
125
+ query GetArticle($id: ID!, $language: String) {
126
+ article(id: $id) {
127
+ articleId
128
+ title
129
+ description
130
+ content
131
+ publishedAt
132
+ releasedBy {
133
+ id
134
+ username
135
+ email
136
+ }
137
+ categories {
138
+ id
139
+ name
140
+ slug
141
+ parentCategory {
142
+ id
143
+ name
144
+ }
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ ```typescript
151
+ // Apollo Client usage
152
+ import { gql, useQuery } from '@apollo/client';
153
+
154
+ const GET_ARTICLE = gql`
155
+ query GetArticle($id: ID!) {
156
+ article(id: $id) {
157
+ articleId
158
+ title
159
+ description
160
+ content
161
+ publishedAt
162
+ categories {
163
+ id
164
+ name
165
+ slug
166
+ }
167
+ }
168
+ }
169
+ `;
170
+
171
+ function ArticlePage({ articleId }: { articleId: string }) {
172
+ const { loading, error, data } = useQuery(GET_ARTICLE, {
173
+ variables: { id: articleId }
174
+ });
175
+
176
+ if (loading) return <div>Loading...</div>;
177
+ if (error) return <div>Error: {error.message}</div>;
178
+
179
+ return (
180
+ <article>
181
+ <h1>{data.article.title}</h1>
182
+ <p>{data.article.description}</p>
183
+ {/* Render Quadrats content */}
184
+ </article>
185
+ );
186
+ }
187
+ ```
188
+
189
+ #### Query Article Collection
190
+
191
+ ```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
+ ) {
205
+ items {
206
+ articleId
207
+ title
208
+ description
209
+ publishedAt
210
+ categories {
211
+ id
212
+ name
213
+ slug
214
+ }
215
+ }
216
+ meta {
217
+ totalCount
218
+ pageCount
219
+ currentPage
220
+ hasNextPage
221
+ hasPreviousPage
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ ```typescript
228
+ // React component with pagination
229
+ import { gql, useQuery } from '@apollo/client';
230
+
231
+ const GET_ARTICLES = gql`
232
+ query GetArticles($page: Int, $limit: Int, $categoryId: ID) {
233
+ articles(page: $page, limit: $limit, categoryId: $categoryId) {
234
+ items {
235
+ articleId
236
+ title
237
+ description
238
+ publishedAt
239
+ }
240
+ meta {
241
+ totalCount
242
+ currentPage
243
+ hasNextPage
244
+ }
245
+ }
246
+ }
247
+ `;
248
+
249
+ function ArticleList() {
250
+ const [page, setPage] = useState(1);
251
+ const { loading, error, data } = useQuery(GET_ARTICLES, {
252
+ variables: { page, limit: 10 }
253
+ });
254
+
255
+ return (
256
+ <div>
257
+ {data?.articles.items.map((article) => (
258
+ <ArticleCard key={article.articleId} article={article} />
259
+ ))}
260
+
261
+ <Pagination
262
+ currentPage={data?.articles.meta.currentPage}
263
+ hasNextPage={data?.articles.meta.hasNextPage}
264
+ onPageChange={setPage}
265
+ />
266
+ </div>
267
+ );
268
+ }
269
+ ```
270
+
271
+ ### Backstage (Admin) Queries
272
+
273
+ #### Query Articles with Management Data
274
+
275
+ ```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
+ ) {
288
+ items {
289
+ articleId
290
+ title
291
+ stage
292
+ createdAt
293
+ updatedAt
294
+ author {
295
+ id
296
+ username
297
+ }
298
+ signature {
299
+ id
300
+ approved
301
+ approvedAt
302
+ level
303
+ reviewer {
304
+ id
305
+ username
306
+ }
307
+ }
308
+ }
309
+ meta {
310
+ totalCount
311
+ pageCount
312
+ }
313
+ }
314
+ }
315
+ ```
316
+
317
+ #### Query Article Versions
318
+
319
+ ```graphql
320
+ query GetArticleVersions($articleId: ID!) {
321
+ backstageArticle(id: $articleId) {
322
+ articleId
323
+ title
324
+ stage
325
+ versions {
326
+ id
327
+ versionNumber
328
+ createdAt
329
+ author {
330
+ username
331
+ }
332
+ changes {
333
+ field
334
+ oldValue
335
+ newValue
336
+ }
337
+ }
338
+ }
339
+ }
340
+ ```
341
+
342
+ ### Mutations
343
+
344
+ #### Create Article
345
+
346
+ ```graphql
347
+ mutation CreateArticle($input: CreateArticleInput!) {
348
+ createArticle(input: $input) {
349
+ articleId
350
+ title
351
+ stage
352
+ createdAt
353
+ }
354
+ }
355
+ ```
356
+
357
+ ```typescript
358
+ // Apollo Client mutation
359
+ import { gql, useMutation } from '@apollo/client';
360
+
361
+ const CREATE_ARTICLE = gql`
362
+ mutation CreateArticle($input: CreateArticleInput!) {
363
+ createArticle(input: $input) {
364
+ articleId
365
+ title
366
+ stage
367
+ }
368
+ }
369
+ `;
370
+
371
+ function CreateArticleForm() {
372
+ const [createArticle, { loading, error }] = useMutation(CREATE_ARTICLE);
373
+
374
+ const handleSubmit = async (formData: any) => {
375
+ try {
376
+ const { data } = await createArticle({
377
+ variables: {
378
+ input: {
379
+ title: {
380
+ 'en-US': formData.titleEn,
381
+ 'zh-TW': formData.titleZh
382
+ },
383
+ content: formData.content,
384
+ categoryIds: formData.categories,
385
+ customFields: formData.customFields
386
+ }
387
+ }
388
+ });
389
+
390
+ console.log('Article created:', data.createArticle);
391
+ } catch (err) {
392
+ console.error('Error creating article:', err);
393
+ }
394
+ };
395
+
396
+ return (
397
+ <form onSubmit={handleSubmit}>
398
+ {/* Form fields */}
399
+ </form>
400
+ );
401
+ }
402
+ ```
403
+
404
+ #### Update Article
405
+
406
+ ```graphql
407
+ mutation UpdateArticle($id: ID!, $input: UpdateArticleInput!) {
408
+ updateArticle(id: $id, input: $input) {
409
+ articleId
410
+ title
411
+ stage
412
+ updatedAt
413
+ }
414
+ }
415
+ ```
416
+
417
+ #### Approve Article
418
+
419
+ ```graphql
420
+ mutation ApproveArticle($articleId: ID!, $level: Int!, $comments: String) {
421
+ approveArticle(
422
+ articleId: $articleId
423
+ level: $level
424
+ comments: $comments
425
+ ) {
426
+ id
427
+ approved
428
+ approvedAt
429
+ level
430
+ comments
431
+ }
432
+ }
433
+ ```
434
+
435
+ ## Multi-Language Support
436
+
437
+ ### Language Context
438
+
439
+ ```typescript
440
+ // Custom decorator for language detection
441
+ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
442
+ import { GqlExecutionContext } from '@nestjs/graphql';
443
+
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
+ );
455
+ ```
456
+
457
+ ### Multi-Language Queries
458
+
459
+ ```graphql
460
+ query GetMultiLanguageArticle($id: ID!) {
461
+ article(id: $id) {
462
+ articleId
463
+ title # Returns title in requested language
464
+ description
465
+ content
466
+ multiLanguageTitle {
467
+ language
468
+ value
469
+ }
470
+ multiLanguageContent {
471
+ language
472
+ value
473
+ }
474
+ }
475
+ }
476
+ ```
477
+
478
+ ```typescript
479
+ // Frontend language switching
480
+ function ArticleWithLanguage({ articleId }: { articleId: string }) {
481
+ const [language, setLanguage] = useState('en-US');
482
+
483
+ const { data } = useQuery(GET_ARTICLE, {
484
+ variables: { id: articleId },
485
+ context: {
486
+ headers: {
487
+ 'Accept-Language': language
488
+ }
489
+ }
490
+ });
491
+
492
+ return (
493
+ <div>
494
+ <LanguageSelector onChange={setLanguage} />
495
+ <article>
496
+ <h1>{data?.article.title}</h1>
497
+ <div>{data?.article.content}</div>
498
+ </article>
499
+ </div>
500
+ );
501
+ }
502
+ ```
503
+
504
+ ## Advanced Features
505
+
506
+ ### Custom Resolvers
507
+
508
+ ```typescript
509
+ // custom-article.resolver.ts
510
+ import { Resolver, Query, ResolveField, Parent } from '@nestjs/graphql';
511
+ import { ArticleDto } from '@rytass/cms-base-nestjs-graphql-module';
512
+
513
+ @Resolver(() => ArticleDto)
514
+ export class CustomArticleResolver {
515
+
516
+ @ResolveField(() => String, { nullable: true })
517
+ async seoTitle(@Parent() article: ArticleDto): Promise<string | null> {
518
+ // Custom SEO title logic
519
+ return article.title + ' | Your Site Name';
520
+ }
521
+
522
+ @ResolveField(() => [String])
523
+ async tags(@Parent() article: ArticleDto): Promise<string[]> {
524
+ // Extract tags from content or custom fields
525
+ return article.customFields?.tags || [];
526
+ }
527
+
528
+ @ResolveField(() => Int)
529
+ async readingTime(@Parent() article: ArticleDto): Promise<number> {
530
+ // Calculate reading time based on content
531
+ const wordCount = this.countWords(article.content);
532
+ return Math.ceil(wordCount / 200); // Assuming 200 WPM
533
+ }
534
+
535
+ private countWords(content: any[]): number {
536
+ // Implement word counting logic for Quadrats content
537
+ return content.reduce((count, block) => {
538
+ if (block.type === 'text') {
539
+ return count + (block.text?.split(' ').length || 0);
540
+ }
541
+ return count;
542
+ }, 0);
543
+ }
544
+ }
545
+ ```
546
+
547
+ ### DataLoader Optimization
548
+
549
+ ```typescript
550
+ // custom-dataloader.service.ts
551
+ import { Injectable } from '@nestjs/common';
552
+ import DataLoader from 'dataloader';
553
+ import { InjectRepository } from '@nestjs/typeorm';
554
+ import { Repository, In } from 'typeorm';
555
+
556
+ @Injectable()
557
+ export class CustomDataLoaderService {
558
+
559
+ // 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
+ );
566
+
567
+ // 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
+ );
574
+
575
+ private async getArticleViewCounts(articleIds: string[]): Promise<Record<string, number>> {
576
+ // Implement batch view count fetching
577
+ return {};
578
+ }
579
+
580
+ private async getRelatedArticles(articleIds: string[]): Promise<Record<string, ArticleDto[]>> {
581
+ // Implement batch related articles fetching
582
+ return {};
583
+ }
584
+ }
585
+ ```
586
+
587
+ ### Search Integration
588
+
589
+ ```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
+ ) {
602
+ items {
603
+ articleId
604
+ title
605
+ description
606
+ searchScore
607
+ highlights {
608
+ field
609
+ snippets
610
+ }
611
+ }
612
+ meta {
613
+ totalCount
614
+ searchTime
615
+ suggestions
616
+ }
617
+ facets {
618
+ categories {
619
+ id
620
+ name
621
+ count
622
+ }
623
+ authors {
624
+ id
625
+ name
626
+ count
627
+ }
628
+ }
629
+ }
630
+ }
631
+ ```
632
+
633
+ ## Permission-Based Access
634
+
635
+ ### Role-Based Queries
636
+
637
+ ```typescript
638
+ // permissions.resolver.ts
639
+ import { Resolver, Query, UseGuards } from '@nestjs/graphql';
640
+ import { AllowActions, RequireActions } from '@rytass/member-base-nestjs-module';
641
+ import { BaseAction } from './constants/enum/base-action.enum';
642
+ import { BaseResource } from './constants/enum/base-resource.enum';
643
+
644
+ @Resolver()
645
+ export class PermissionResolver {
646
+
647
+ @Query(() => [BackstageArticleDto])
648
+ @RequireActions([BaseAction.READ], BaseResource.ARTICLE)
649
+ async getAllArticles(): Promise<BackstageArticleDto[]> {
650
+ // Only accessible to users with READ permission on ARTICLE resource
651
+ return this.articleService.findAll();
652
+ }
653
+
654
+ @Query(() => BackstageArticleDto)
655
+ @RequireActions([BaseAction.UPDATE], BaseResource.ARTICLE)
656
+ async getEditableArticle(@Args('id') id: string): Promise<BackstageArticleDto> {
657
+ // Only accessible to users with UPDATE permission
658
+ return this.articleService.findEditableById(id);
659
+ }
660
+ }
661
+ ```
662
+
663
+ ### Context-Based Security
664
+
665
+ ```typescript
666
+ // security.resolver.ts
667
+ import { Resolver, Query, Context } from '@nestjs/graphql';
668
+
669
+ @Resolver()
670
+ export class SecurityResolver {
671
+
672
+ @Query(() => [ArticleDto])
673
+ async getUserArticles(@Context() context: any): Promise<ArticleDto[]> {
674
+ const userId = context.req.user?.id;
675
+
676
+ if (!userId) {
677
+ throw new UnauthorizedException('User not authenticated');
678
+ }
679
+
680
+ return this.articleService.findByAuthor(userId);
681
+ }
682
+ }
683
+ ```
684
+
685
+ ## Integration Examples
686
+
687
+ ### Frontend Integration (React + Apollo)
688
+
689
+ ```typescript
690
+ // ArticleManagement.tsx
691
+ import React from 'react';
692
+ import { useQuery, useMutation } from '@apollo/client';
693
+ import { GET_BACKSTAGE_ARTICLES, APPROVE_ARTICLE } from './queries';
694
+
695
+ export function ArticleManagement() {
696
+ const { data, loading, refetch } = useQuery(GET_BACKSTAGE_ARTICLES, {
697
+ variables: { page: 1, limit: 20, stage: 'PENDING' }
698
+ });
699
+
700
+ const [approveArticle] = useMutation(APPROVE_ARTICLE, {
701
+ onCompleted: () => refetch()
702
+ });
703
+
704
+ const handleApprove = async (articleId: string) => {
705
+ await approveArticle({
706
+ variables: {
707
+ articleId,
708
+ level: 2,
709
+ comments: 'Approved for publication'
710
+ }
711
+ });
712
+ };
713
+
714
+ if (loading) return <div>Loading...</div>;
715
+
716
+ return (
717
+ <div>
718
+ <h2>Pending Articles</h2>
719
+ {data?.backstageArticles.items.map((article: any) => (
720
+ <ArticleCard
721
+ key={article.articleId}
722
+ article={article}
723
+ onApprove={() => handleApprove(article.articleId)}
724
+ />
725
+ ))}
726
+ </div>
727
+ );
728
+ }
729
+ ```
730
+
731
+ ### Mobile App Integration (React Native + Apollo)
732
+
733
+ ```typescript
734
+ // ArticleList.tsx
735
+ import React from 'react';
736
+ import { FlatList, Text, View } from 'react-native';
737
+ import { useQuery } from '@apollo/client';
738
+ import { GET_ARTICLES } from './queries';
739
+
740
+ export function ArticleList() {
741
+ const { data, loading, fetchMore } = useQuery(GET_ARTICLES, {
742
+ variables: { page: 1, limit: 10 }
743
+ });
744
+
745
+ const loadMore = () => {
746
+ if (data?.articles.meta.hasNextPage) {
747
+ fetchMore({
748
+ variables: {
749
+ page: data.articles.meta.currentPage + 1
750
+ },
751
+ updateQuery: (prev, { fetchMoreResult }) => {
752
+ if (!fetchMoreResult) return prev;
753
+
754
+ return {
755
+ articles: {
756
+ ...fetchMoreResult.articles,
757
+ items: [
758
+ ...prev.articles.items,
759
+ ...fetchMoreResult.articles.items
760
+ ]
761
+ }
762
+ };
763
+ }
764
+ });
765
+ }
766
+ };
767
+
768
+ return (
769
+ <FlatList
770
+ data={data?.articles.items}
771
+ renderItem={({ item }) => (
772
+ <View>
773
+ <Text>{item.title}</Text>
774
+ <Text>{item.description}</Text>
775
+ </View>
776
+ )}
777
+ onEndReached={loadMore}
778
+ onEndReachedThreshold={0.5}
779
+ />
780
+ );
781
+ }
782
+ ```
783
+
784
+ ### Next.js SSR Integration
785
+
786
+ ```typescript
787
+ // pages/articles/[id].tsx
788
+ import { GetServerSideProps } from 'next';
789
+ import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
790
+
791
+ export default function ArticlePage({ article }: { article: any }) {
792
+ return (
793
+ <div>
794
+ <h1>{article.title}</h1>
795
+ <div>{article.description}</div>
796
+ {/* Render article content */}
797
+ </div>
798
+ );
799
+ }
800
+
801
+ export const getServerSideProps: GetServerSideProps = async ({ params, req }) => {
802
+ const client = new ApolloClient({
803
+ uri: process.env.GRAPHQL_ENDPOINT,
804
+ cache: new InMemoryCache(),
805
+ headers: {
806
+ 'Accept-Language': req.headers['accept-language'] || 'en-US'
807
+ }
808
+ });
809
+
810
+ const { data } = await client.query({
811
+ query: gql`
812
+ query GetArticle($id: ID!) {
813
+ article(id: $id) {
814
+ articleId
815
+ title
816
+ description
817
+ content
818
+ }
819
+ }
820
+ `,
821
+ variables: { id: params?.id }
822
+ });
823
+
824
+ return {
825
+ props: {
826
+ article: data.article
827
+ }
828
+ };
829
+ };
830
+ ```
831
+
832
+ ## Performance Optimization
833
+
834
+ ### Query Complexity Analysis
835
+
836
+ ```typescript
837
+ // complexity.config.ts
838
+ import { createComplexityLimitRule } from 'graphql-query-complexity';
839
+
840
+ export const complexityConfig = {
841
+ maximumComplexity: 1000,
842
+ validators: [createComplexityLimitRule(1000)],
843
+ createError: (max: number, actual: number) => {
844
+ return new Error(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`);
845
+ }
846
+ };
847
+ ```
848
+
849
+ ### Caching Strategy
850
+
851
+ ```typescript
852
+ // cache.config.ts
853
+ import { CacheModule } from '@nestjs/cache-manager';
854
+ import { redisStore } from 'cache-manager-redis-store';
855
+
856
+ @Module({
857
+ imports: [
858
+ CacheModule.register({
859
+ store: redisStore,
860
+ host: 'localhost',
861
+ port: 6379,
862
+ ttl: 600, // 10 minutes
863
+ })
864
+ ]
865
+ })
866
+ export class CacheConfig {}
867
+
868
+ // Cached resolver
869
+ @Resolver()
870
+ export class CachedArticleResolver {
871
+
872
+ @Query(() => [ArticleDto])
873
+ @UseInterceptors(CacheInterceptor)
874
+ @CacheTTL(300) // 5 minutes
875
+ async popularArticles(): Promise<ArticleDto[]> {
876
+ return this.articleService.findPopular();
877
+ }
878
+ }
879
+ ```
880
+
881
+ ## Testing
882
+
883
+ ### GraphQL Testing
884
+
885
+ ```typescript
886
+ // article.resolver.spec.ts
887
+ import { Test, TestingModule } from '@nestjs/testing';
888
+ import { INestApplication } from '@nestjs/common';
889
+ import * as request from 'supertest';
890
+ import { GraphQLModule } from '@nestjs/graphql';
891
+
892
+ describe('Article Resolver (e2e)', () => {
893
+ let app: INestApplication;
894
+
895
+ beforeEach(async () => {
896
+ const moduleFixture: TestingModule = await Test.createTestingModule({
897
+ imports: [
898
+ GraphQLModule.forRoot({
899
+ autoSchemaFile: true,
900
+ }),
901
+ CMSBaseGraphQLModule.forRoot({
902
+ multipleLanguageMode: false,
903
+ draftMode: true,
904
+ })
905
+ ],
906
+ }).compile();
907
+
908
+ app = moduleFixture.createNestApplication();
909
+ await app.init();
910
+ });
911
+
912
+ it('should get article by id', () => {
913
+ const query = `
914
+ query {
915
+ article(id: "test-id") {
916
+ articleId
917
+ title
918
+ description
919
+ }
920
+ }
921
+ `;
922
+
923
+ return request(app.getHttpServer())
924
+ .post('/graphql')
925
+ .send({ query })
926
+ .expect(200)
927
+ .expect((res) => {
928
+ expect(res.body.data.article).toBeDefined();
929
+ expect(res.body.data.article.articleId).toBe('test-id');
930
+ });
931
+ });
932
+ });
933
+ ```
934
+
935
+ ### DataLoader Testing
936
+
937
+ ```typescript
938
+ // dataloader.spec.ts
939
+ import { Test } from '@nestjs/testing';
940
+ import { ArticleDataLoader } from '../data-loaders/article.dataloader';
941
+
942
+ describe('ArticleDataLoader', () => {
943
+ let dataLoader: ArticleDataLoader;
944
+
945
+ beforeEach(async () => {
946
+ const module = await Test.createTestingModule({
947
+ providers: [ArticleDataLoader]
948
+ }).compile();
949
+
950
+ dataLoader = module.get<ArticleDataLoader>(ArticleDataLoader);
951
+ });
952
+
953
+ it('should batch load categories', async () => {
954
+ const articleIds = ['article1', 'article2'];
955
+ const categories = await dataLoader.categoriesLoader.loadMany(
956
+ articleIds.map(id => ({ articleId: id, language: 'en-US' }))
957
+ );
958
+
959
+ expect(categories).toHaveLength(2);
960
+ expect(categories[0]).toBeInstanceOf(Array);
961
+ });
962
+ });
963
+ ```
964
+
965
+ ## Best Practices
966
+
967
+ ### Schema Design
968
+ - Use consistent naming conventions for all GraphQL types
969
+ - Implement proper pagination for all collection queries
970
+ - Design efficient DataLoader patterns to prevent N+1 queries
971
+ - Use nullable fields appropriately to handle missing data
972
+
973
+ ### Performance
974
+ - Implement query complexity analysis to prevent expensive queries
975
+ - Use DataLoader for all relationship queries
976
+ - Cache frequently accessed data with appropriate TTL
977
+ - Optimize database queries with proper indexing
978
+
979
+ ### Security
980
+ - Implement proper authentication and authorization
981
+ - Validate all input parameters and payloads
982
+ - Use rate limiting to prevent abuse
983
+ - Sanitize user-generated content
984
+
985
+ ### Development
986
+ - Write comprehensive tests for all resolvers
987
+ - Use TypeScript for type safety across the GraphQL schema
988
+ - Implement proper error handling and logging
989
+ - Follow GraphQL best practices for schema evolution
990
+
991
+ ## Environment Configuration
992
+
993
+ ```bash
994
+ # .env
995
+ DATABASE_HOST=localhost
996
+ DATABASE_PORT=5432
997
+ DATABASE_NAME=cms_db
998
+ DATABASE_USER=cms_user
999
+ DATABASE_PASSWORD=secure_password
1000
+
1001
+ CMS_MULTI_LANGUAGE=true
1002
+ CMS_DRAFT_MODE=true
1003
+ CMS_FULL_TEXT_SEARCH=true
1004
+ CMS_AUTO_RELEASE=false
1005
+
1006
+ REDIS_HOST=localhost
1007
+ REDIS_PORT=6379
1008
+ REDIS_PASSWORD=redis_password
1009
+
1010
+ GRAPHQL_PLAYGROUND=true
1011
+ GRAPHQL_INTROSPECTION=true
1012
+ ```
1013
+
1014
+ ## License
1015
+
1016
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/cms-base-nestjs-graphql-module",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Rytass Content Management System NestJS Base GraphQL Module",
5
5
  "keywords": [
6
6
  "rytass",
@@ -26,8 +26,8 @@
26
26
  "typeorm": "*"
27
27
  },
28
28
  "dependencies": {
29
- "@rytass/cms-base-nestjs-module": "^0.2.2",
30
- "@rytass/member-base-nestjs-module": "^0.2.7",
29
+ "@rytass/cms-base-nestjs-module": "^0.2.4",
30
+ "@rytass/member-base-nestjs-module": "^0.2.8",
31
31
  "dataloader": "^2.2.2",
32
32
  "lru-cache": "^11.0.2"
33
33
  },