@rytass/cms-base-nestjs-graphql-module 0.1.24 → 0.1.26
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/CHANGELOG.md +306 -0
- package/__tests__/enums.spec.ts +76 -0
- package/__tests__/language-decorator.spec.ts +17 -0
- package/__tests__/quadrats-element-scalar.spec.ts +123 -0
- package/llms.txt +361 -0
- package/package.json +10 -6
- package/project.json +23 -0
- package/src/cms-base-graphql.module.ts +101 -0
- package/src/constants/enum/base-action.enum.ts +17 -0
- package/src/constants/enum/base-resource.enum.ts +4 -0
- package/src/constants/option-providers.ts +35 -0
- package/src/data-loaders/article.dataloader.ts +84 -0
- package/src/data-loaders/members.dataloader.ts +36 -0
- package/src/decorators/language.decorator.ts +45 -0
- package/src/dto/article-collection.dto.ts +9 -0
- package/src/dto/article-multi-language-content.dto.ts +18 -0
- package/src/dto/article-signature-step.dto.ts +10 -0
- package/src/dto/article-signature.dto.ts +21 -0
- package/src/dto/article-stage-version.dto.ts +23 -0
- package/src/dto/article-version-content.input.ts +18 -0
- package/src/dto/article.dto.ts +16 -0
- package/src/dto/articles.args.ts +16 -0
- package/src/dto/backstage-article-collection.dto.ts +9 -0
- package/src/dto/backstage-article.args.ts +20 -0
- package/src/dto/backstage-article.dto.ts +14 -0
- package/src/dto/backstage-category.dto.ts +5 -0
- package/src/dto/base-article.dto.ts +19 -0
- package/src/dto/base-category.dto.ts +13 -0
- package/src/dto/categories.args.ts +27 -0
- package/src/dto/category-multi-language-name.dto.ts +10 -0
- package/src/dto/category-multi-language-name.input.ts +10 -0
- package/src/dto/category.dto.ts +8 -0
- package/src/dto/collection.dto.ts +13 -0
- package/src/dto/create-article.args.ts +27 -0
- package/src/dto/create-category.args.ts +15 -0
- package/src/dto/custom-field.input.ts +10 -0
- package/src/dto/update-article.args.ts +8 -0
- package/src/dto/update-category.args.ts +8 -0
- package/src/dto/user.dto.ts +10 -0
- package/{index.d.ts → src/index.ts} +2 -0
- package/src/mutations/article.mutations.ts +177 -0
- package/src/mutations/category.mutations.ts +78 -0
- package/src/queries/article.queries.ts +71 -0
- package/src/queries/category.queries.ts +53 -0
- package/src/resolvers/article-signature.resolver.ts +29 -0
- package/src/resolvers/article.resolver.ts +41 -0
- package/src/resolvers/backstage-article.resolver.ts +161 -0
- package/src/resolvers/backstage-category.resolver.ts +40 -0
- package/src/scalars/quadrats-element.scalar.ts +56 -0
- package/src/typings/cms-graphql-base-providers.ts +3 -0
- package/{typings/cms-graphql-base-root-module-async-options.dto.d.ts → src/typings/cms-graphql-base-root-module-async-options.dto.ts} +6 -4
- package/{typings/cms-graphql-base-root-module-option-factory.d.ts → src/typings/cms-graphql-base-root-module-option-factory.ts} +2 -1
- package/src/typings/cms-graphql-base-root-module-options.dto.ts +11 -0
- package/src/typings/dto/resolved-create-article-args.dto.ts +33 -0
- package/src/typings/dto/resolved-create-category-args.dto.ts +19 -0
- package/tsconfig.build.json +8 -0
- package/LICENSE +0 -21
- package/cms-base-graphql.module.d.ts +0 -9
- package/cms-base-graphql.module.js +0 -114
- package/constants/enum/base-action.enum.d.ts +0 -14
- package/constants/enum/base-action.enum.js +0 -17
- package/constants/enum/base-resource.enum.d.ts +0 -4
- package/constants/enum/base-resource.enum.js +0 -7
- package/constants/option-providers.d.ts +0 -2
- package/constants/option-providers.js +0 -20
- package/data-loaders/article.dataloader.d.ts +0 -17
- package/data-loaders/article.dataloader.js +0 -68
- package/data-loaders/members.dataloader.d.ts +0 -8
- package/data-loaders/members.dataloader.js +0 -56
- package/decorators/language.decorator.d.ts +0 -2
- package/decorators/language.decorator.js +0 -32
- package/dto/article-collection.dto.d.ts +0 -5
- package/dto/article-collection.dto.js +0 -27
- package/dto/article-multi-language-content.dto.d.ts +0 -7
- package/dto/article-multi-language-content.dto.js +0 -41
- package/dto/article-signature-step.dto.d.ts +0 -4
- package/dto/article-signature-step.dto.js +0 -28
- package/dto/article-signature.dto.d.ts +0 -7
- package/dto/article-signature.dto.js +0 -44
- package/dto/article-stage-version.dto.d.ts +0 -9
- package/dto/article-stage-version.dto.js +0 -59
- package/dto/article-version-content.input.d.ts +0 -7
- package/dto/article-version-content.input.js +0 -43
- package/dto/article.dto.d.ts +0 -7
- package/dto/article.dto.js +0 -37
- package/dto/articles.args.d.ts +0 -6
- package/dto/articles.args.js +0 -48
- package/dto/backstage-article-collection.dto.d.ts +0 -5
- package/dto/backstage-article-collection.dto.js +0 -27
- package/dto/backstage-article.args.d.ts +0 -6
- package/dto/backstage-article.args.js +0 -38
- package/dto/backstage-article.dto.d.ts +0 -6
- package/dto/backstage-article.dto.js +0 -38
- package/dto/backstage-category.dto.d.ts +0 -3
- package/dto/backstage-category.dto.js +0 -16
- package/dto/base-article.dto.d.ts +0 -7
- package/dto/base-article.dto.js +0 -47
- package/dto/base-category.dto.d.ts +0 -5
- package/dto/base-category.dto.js +0 -33
- package/dto/categories.args.d.ts +0 -8
- package/dto/categories.args.js +0 -62
- package/dto/category-multi-language-name.dto.d.ts +0 -4
- package/dto/category-multi-language-name.dto.js +0 -28
- package/dto/category-multi-language-name.input.d.ts +0 -4
- package/dto/category-multi-language-name.input.js +0 -30
- package/dto/category.dto.d.ts +0 -4
- package/dto/category.dto.js +0 -24
- package/dto/collection.dto.d.ts +0 -5
- package/dto/collection.dto.js +0 -33
- package/dto/create-article.args.d.ts +0 -11
- package/dto/create-article.args.js +0 -71
- package/dto/create-category.args.d.ts +0 -7
- package/dto/create-category.args.js +0 -45
- package/dto/custom-field.input.d.ts +0 -4
- package/dto/custom-field.input.js +0 -28
- package/dto/update-article.args.d.ts +0 -4
- package/dto/update-article.args.js +0 -24
- package/dto/update-category.args.d.ts +0 -4
- package/dto/update-category.args.js +0 -24
- package/dto/user.dto.d.ts +0 -4
- package/dto/user.dto.js +0 -28
- package/index.cjs.js +0 -2334
- package/index.js +0 -8
- package/mutations/article.mutations.d.ts +0 -22
- package/mutations/article.mutations.js +0 -338
- package/mutations/category.mutations.d.ts +0 -15
- package/mutations/category.mutations.js +0 -134
- package/queries/article.queries.d.ts +0 -16
- package/queries/article.queries.js +0 -132
- package/queries/category.queries.d.ts +0 -13
- package/queries/category.queries.js +0 -124
- package/resolvers/article-signature.resolver.d.ts +0 -10
- package/resolvers/article-signature.resolver.js +0 -68
- package/resolvers/article.resolver.d.ts +0 -14
- package/resolvers/article.resolver.js +0 -95
- package/resolvers/backstage-article.resolver.d.ts +0 -33
- package/resolvers/backstage-article.resolver.js +0 -296
- package/resolvers/backstage-category.resolver.d.ts +0 -8
- package/resolvers/backstage-category.resolver.js +0 -76
- package/scalars/quadrats-element.scalar.d.ts +0 -3
- package/scalars/quadrats-element.scalar.js +0 -47
- package/typings/cms-graphql-base-providers.d.ts +0 -3
- package/typings/cms-graphql-base-providers.js +0 -5
- package/typings/cms-graphql-base-root-module-options.dto.d.ts +0 -6
- package/typings/dto/resolved-create-article-args.dto.d.ts +0 -24
- package/typings/dto/resolved-create-category-args.dto.d.ts +0 -13
- /package/{typings/custom-field-value.type.d.ts → src/typings/custom-field-value.type.ts} +0 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# @rytass/cms-base-nestjs-graphql-module
|
|
2
|
+
|
|
3
|
+
Rytass Content Management System NestJS GraphQL Module - A GraphQL extension for the CMS base module providing GraphQL resolvers, queries, mutations, and DTOs for building GraphQL-enabled CMS applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @rytass/cms-base-nestjs-graphql-module
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### GraphQL Resolvers
|
|
14
|
+
- **ArticleResolver**: Basic article field resolution with author and category relationships
|
|
15
|
+
- **BackstageArticleResolver**: Administrative article management operations
|
|
16
|
+
- **ArticleSignatureResolver**: Article approval workflow resolution
|
|
17
|
+
- **BackstageCategoryResolver**: Administrative category management
|
|
18
|
+
|
|
19
|
+
### Queries & Mutations
|
|
20
|
+
- **ArticleQueries**: Public article data queries
|
|
21
|
+
- **CategoryQueries**: Public category data queries
|
|
22
|
+
- **ArticleMutations**: Article CRUD operations
|
|
23
|
+
- **CategoryMutations**: Category CRUD operations
|
|
24
|
+
|
|
25
|
+
### Data Loaders
|
|
26
|
+
- **ArticleDataLoader**: Efficient GraphQL article loading
|
|
27
|
+
- **MemberDataLoader**: User/author data loading for article relationships
|
|
28
|
+
|
|
29
|
+
### GraphQL DTOs
|
|
30
|
+
- **ArticleDto**: Public article representation
|
|
31
|
+
- **BackstageArticleDto**: Administrative article with full metadata
|
|
32
|
+
- **CategoryDto**: Public category representation
|
|
33
|
+
- **BackstageCategoryDto**: Administrative category operations
|
|
34
|
+
- **ArticleCollectionDto**: Paginated article collections
|
|
35
|
+
|
|
36
|
+
### Custom Scalars
|
|
37
|
+
- **QuadratsElementScalar**: Support for rich content editor elements
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Basic Setup
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { Module } from '@nestjs/common';
|
|
45
|
+
import { GraphQLModule } from '@nestjs/graphql';
|
|
46
|
+
import { CMSBaseGraphQLModule } from '@rytass/cms-base-nestjs-graphql-module';
|
|
47
|
+
|
|
48
|
+
@Module({
|
|
49
|
+
imports: [
|
|
50
|
+
GraphQLModule.forRoot({
|
|
51
|
+
autoSchemaFile: true,
|
|
52
|
+
}),
|
|
53
|
+
CMSBaseGraphQLModule.forRoot({
|
|
54
|
+
multipleLanguageMode: true,
|
|
55
|
+
enableDraftMode: true,
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
58
|
+
})
|
|
59
|
+
export class AppModule {}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Async Configuration
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
@Module({
|
|
66
|
+
imports: [
|
|
67
|
+
CMSBaseGraphQLModule.forRootAsync({
|
|
68
|
+
imports: [ConfigModule],
|
|
69
|
+
useFactory: (config: ConfigService) => ({
|
|
70
|
+
multipleLanguageMode: config.get('CMS_MULTI_LANG'),
|
|
71
|
+
enableDraftMode: config.get('CMS_DRAFT_MODE'),
|
|
72
|
+
}),
|
|
73
|
+
inject: [ConfigService],
|
|
74
|
+
}),
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
export class AppModule {}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## GraphQL Schema Examples
|
|
81
|
+
|
|
82
|
+
### Article Queries
|
|
83
|
+
|
|
84
|
+
```graphql
|
|
85
|
+
# Get public articles
|
|
86
|
+
query GetArticles($first: Int!, $after: String) {
|
|
87
|
+
articles(first: $first, after: $after) {
|
|
88
|
+
edges {
|
|
89
|
+
node {
|
|
90
|
+
articleId
|
|
91
|
+
title
|
|
92
|
+
content
|
|
93
|
+
publishedAt
|
|
94
|
+
author {
|
|
95
|
+
id
|
|
96
|
+
name
|
|
97
|
+
}
|
|
98
|
+
categories {
|
|
99
|
+
id
|
|
100
|
+
name
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
pageInfo {
|
|
105
|
+
hasNextPage
|
|
106
|
+
endCursor
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Get single article
|
|
112
|
+
query GetArticle($id: ID!) {
|
|
113
|
+
article(id: $id) {
|
|
114
|
+
articleId
|
|
115
|
+
title
|
|
116
|
+
content
|
|
117
|
+
publishedAt
|
|
118
|
+
categories {
|
|
119
|
+
id
|
|
120
|
+
name
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Backstage Operations
|
|
127
|
+
|
|
128
|
+
```graphql
|
|
129
|
+
# Create article (admin)
|
|
130
|
+
mutation CreateArticle($input: CreateArticleInput!) {
|
|
131
|
+
createArticle(input: $input) {
|
|
132
|
+
id
|
|
133
|
+
title
|
|
134
|
+
stage
|
|
135
|
+
createdAt
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Update article (admin)
|
|
140
|
+
mutation UpdateArticle($id: ID!, $input: UpdateArticleInput!) {
|
|
141
|
+
updateArticle(id: $id, input: $input) {
|
|
142
|
+
id
|
|
143
|
+
title
|
|
144
|
+
updatedAt
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Category Operations
|
|
150
|
+
|
|
151
|
+
```graphql
|
|
152
|
+
# Get categories
|
|
153
|
+
query GetCategories {
|
|
154
|
+
categories {
|
|
155
|
+
id
|
|
156
|
+
name
|
|
157
|
+
slug
|
|
158
|
+
parent {
|
|
159
|
+
id
|
|
160
|
+
name
|
|
161
|
+
}
|
|
162
|
+
children {
|
|
163
|
+
id
|
|
164
|
+
name
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Create category (admin)
|
|
170
|
+
mutation CreateCategory($input: CreateCategoryInput!) {
|
|
171
|
+
createCategory(input: $input) {
|
|
172
|
+
id
|
|
173
|
+
name
|
|
174
|
+
slug
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## TypeScript Integration
|
|
180
|
+
|
|
181
|
+
### Using GraphQL DTOs
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { Field, ObjectType, ID } from '@nestjs/graphql';
|
|
185
|
+
import { ArticleDto, CategoryDto } from '@rytass/cms-base-nestjs-graphql-module';
|
|
186
|
+
|
|
187
|
+
@ObjectType()
|
|
188
|
+
export class CustomArticleDto extends ArticleDto {
|
|
189
|
+
@Field(() => String, { nullable: true })
|
|
190
|
+
customField?: string;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Custom Resolvers
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { Resolver, Query, Args } from '@nestjs/graphql';
|
|
198
|
+
import { ArticleDto } from '@rytass/cms-base-nestjs-graphql-module';
|
|
199
|
+
|
|
200
|
+
@Resolver(() => ArticleDto)
|
|
201
|
+
export class CustomArticleResolver {
|
|
202
|
+
@Query(() => [ArticleDto])
|
|
203
|
+
async featuredArticles(): Promise<ArticleDto[]> {
|
|
204
|
+
// custom logic
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Configuration Options
|
|
211
|
+
|
|
212
|
+
Inherits all options from `@rytass/cms-base-nestjs-module` plus:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
interface CMSGraphqlBaseModuleOptionsDto extends CMSBaseModuleOptionsDto {
|
|
216
|
+
// All base CMS options are available
|
|
217
|
+
multipleLanguageMode?: boolean;
|
|
218
|
+
enableDraftMode?: boolean;
|
|
219
|
+
// ... other CMS base options
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Key GraphQL Types
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// Article representation
|
|
227
|
+
@ObjectType()
|
|
228
|
+
export class ArticleDto {
|
|
229
|
+
@Field(() => ID)
|
|
230
|
+
articleId: string;
|
|
231
|
+
|
|
232
|
+
@Field()
|
|
233
|
+
title: string;
|
|
234
|
+
|
|
235
|
+
@Field()
|
|
236
|
+
content: string;
|
|
237
|
+
|
|
238
|
+
@Field(() => Date, { nullable: true })
|
|
239
|
+
publishedAt?: Date;
|
|
240
|
+
|
|
241
|
+
@Field(() => [CategoryDto])
|
|
242
|
+
categories: CategoryDto[];
|
|
243
|
+
|
|
244
|
+
@Field(() => UserDto, { nullable: true })
|
|
245
|
+
author?: UserDto;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Category representation
|
|
249
|
+
@ObjectType()
|
|
250
|
+
export class CategoryDto {
|
|
251
|
+
@Field(() => ID)
|
|
252
|
+
id: string;
|
|
253
|
+
|
|
254
|
+
@Field()
|
|
255
|
+
name: string;
|
|
256
|
+
|
|
257
|
+
@Field()
|
|
258
|
+
slug: string;
|
|
259
|
+
|
|
260
|
+
@Field(() => CategoryDto, { nullable: true })
|
|
261
|
+
parent?: CategoryDto;
|
|
262
|
+
|
|
263
|
+
@Field(() => [CategoryDto])
|
|
264
|
+
children: CategoryDto[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Collection with pagination
|
|
268
|
+
@ObjectType()
|
|
269
|
+
export class ArticleCollectionDto {
|
|
270
|
+
@Field(() => [ArticleDto])
|
|
271
|
+
edges: ArticleDto[];
|
|
272
|
+
|
|
273
|
+
@Field()
|
|
274
|
+
totalCount: number;
|
|
275
|
+
|
|
276
|
+
@Field(() => PageInfo)
|
|
277
|
+
pageInfo: PageInfo;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Data Loading
|
|
282
|
+
|
|
283
|
+
The module uses DataLoader pattern for efficient N+1 query prevention:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Automatic batched loading
|
|
287
|
+
@ResolveField(() => UserDto)
|
|
288
|
+
async author(@Root() article: ArticleDto): Promise<UserDto> {
|
|
289
|
+
return this.memberDataLoader.load(article.authorId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@ResolveField(() => [CategoryDto])
|
|
293
|
+
async categories(@Root() article: ArticleDto): Promise<CategoryDto[]> {
|
|
294
|
+
return this.articleDataLoader.loadCategories(article.id);
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Language Support
|
|
299
|
+
|
|
300
|
+
When `multipleLanguageMode` is enabled:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Language-aware resolvers
|
|
304
|
+
@ResolveField(() => String)
|
|
305
|
+
title(
|
|
306
|
+
@Root() article: ArticleDto,
|
|
307
|
+
@Language() language: string
|
|
308
|
+
): string {
|
|
309
|
+
return this.getLocalizedContent(article, language, 'title');
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Dependencies
|
|
314
|
+
|
|
315
|
+
- `@rytass/cms-base-nestjs-module`: Core CMS functionality
|
|
316
|
+
- `@rytass/member-base-nestjs-module`: User management integration
|
|
317
|
+
- `dataloader`: Efficient data loading
|
|
318
|
+
- `lru-cache`: Caching support
|
|
319
|
+
|
|
320
|
+
## Peer Dependencies
|
|
321
|
+
|
|
322
|
+
- `@nestjs/common`: NestJS framework
|
|
323
|
+
- `@nestjs/typeorm`: Database integration
|
|
324
|
+
- `@node-rs/jieba`: Text processing
|
|
325
|
+
- `@quadrats/core`: Rich content support
|
|
326
|
+
- `typeorm`: ORM
|
|
327
|
+
|
|
328
|
+
## Testing
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { Test } from '@nestjs/testing';
|
|
332
|
+
import { ArticleResolver } from '@rytass/cms-base-nestjs-graphql-module';
|
|
333
|
+
|
|
334
|
+
describe('ArticleResolver', () => {
|
|
335
|
+
let resolver: ArticleResolver;
|
|
336
|
+
|
|
337
|
+
beforeEach(async () => {
|
|
338
|
+
const module = await Test.createTestingModule({
|
|
339
|
+
providers: [
|
|
340
|
+
ArticleResolver,
|
|
341
|
+
{
|
|
342
|
+
provide: 'MemberDataLoader',
|
|
343
|
+
useValue: mockDataLoader,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
}).compile();
|
|
347
|
+
|
|
348
|
+
resolver = module.get<ArticleResolver>(ArticleResolver);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// tests...
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## License
|
|
356
|
+
|
|
357
|
+
MIT
|
|
358
|
+
|
|
359
|
+
## Support
|
|
360
|
+
|
|
361
|
+
For issues and questions, visit the [GitHub repository](https://github.com/Rytass/Utils/issues).
|
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.26",
|
|
4
4
|
"description": "Rytass Content Management System NestJS Base GraphQL Module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rytass",
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
"type": "git",
|
|
16
16
|
"url": "git+https://github.com/Rytass/Utils.git"
|
|
17
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "nx clean",
|
|
20
|
+
"build": "nx build",
|
|
21
|
+
"lint": "nx lint",
|
|
22
|
+
"test": "nx test",
|
|
23
|
+
"test:watch": "nx test:watch"
|
|
24
|
+
},
|
|
18
25
|
"bugs": {
|
|
19
26
|
"url": "https://github.com/Rytass/Utils/issues"
|
|
20
27
|
},
|
|
@@ -26,12 +33,9 @@
|
|
|
26
33
|
"typeorm": "*"
|
|
27
34
|
},
|
|
28
35
|
"dependencies": {
|
|
29
|
-
"@rytass/cms-base-nestjs-module": "^0.2.
|
|
36
|
+
"@rytass/cms-base-nestjs-module": "^0.2.14",
|
|
30
37
|
"@rytass/member-base-nestjs-module": "^0.2.18",
|
|
31
38
|
"dataloader": "^2.2.3",
|
|
32
39
|
"lru-cache": "^11.2.4"
|
|
33
|
-
}
|
|
34
|
-
"main": "./index.cjs.js",
|
|
35
|
-
"module": "./index.js",
|
|
36
|
-
"typings": "./index.d.ts"
|
|
40
|
+
}
|
|
37
41
|
}
|
package/project.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rytass/cms-base-nestjs-graphql-module",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"projectType": "library",
|
|
5
|
+
"sourceRoot": "packages/cms-base-nestjs-graphql-module/src",
|
|
6
|
+
"targets": {
|
|
7
|
+
"test": {
|
|
8
|
+
"executor": "@nx/jest:jest"
|
|
9
|
+
},
|
|
10
|
+
"test:watch": {
|
|
11
|
+
"executor": "@nx/jest:jest"
|
|
12
|
+
},
|
|
13
|
+
"lint": {
|
|
14
|
+
"executor": "nx:run-commands"
|
|
15
|
+
},
|
|
16
|
+
"clean": {
|
|
17
|
+
"executor": "nx:run-commands"
|
|
18
|
+
},
|
|
19
|
+
"build": {
|
|
20
|
+
"executor": "nx:run-commands"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Module, DynamicModule, Provider, Type } from '@nestjs/common';
|
|
2
|
+
import { CMSBaseModule } from '@rytass/cms-base-nestjs-module';
|
|
3
|
+
import { ArticleMutations } from './mutations/article.mutations';
|
|
4
|
+
import { CategoryMutations } from './mutations/category.mutations';
|
|
5
|
+
import { ArticleQueries } from './queries/article.queries';
|
|
6
|
+
import { CategoryQueries } from './queries/category.queries';
|
|
7
|
+
import { MemberDataLoader } from './data-loaders/members.dataloader';
|
|
8
|
+
import { ArticleDataLoader } from './data-loaders/article.dataloader';
|
|
9
|
+
import { ArticleResolver } from './resolvers/article.resolver';
|
|
10
|
+
import { BackstageArticleResolver } from './resolvers/backstage-article.resolver';
|
|
11
|
+
import { ArticleSignatureResolver } from './resolvers/article-signature.resolver';
|
|
12
|
+
import { BackstageCategoryResolver } from './resolvers/backstage-category.resolver';
|
|
13
|
+
import { CMSGraphqlBaseModuleOptionsDto } from './typings/cms-graphql-base-root-module-options.dto';
|
|
14
|
+
import { CMSGraphqlBaseModuleAsyncOptionsDto } from './typings/cms-graphql-base-root-module-async-options.dto';
|
|
15
|
+
import { OptionProviders } from './constants/option-providers';
|
|
16
|
+
import { CMS_BASE_GRAPHQL_MODULE_OPTIONS } from './typings/cms-graphql-base-providers';
|
|
17
|
+
import { CMSGraphqlBaseModuleOptionFactory } from './typings/cms-graphql-base-root-module-option-factory';
|
|
18
|
+
|
|
19
|
+
@Module({})
|
|
20
|
+
export class CMSBaseGraphQLModule {
|
|
21
|
+
static forRootAsync(options: CMSGraphqlBaseModuleAsyncOptionsDto): DynamicModule {
|
|
22
|
+
return {
|
|
23
|
+
module: CMSBaseGraphQLModule,
|
|
24
|
+
imports: [...(options.imports || []), CMSBaseModule.forRootAsync(options)],
|
|
25
|
+
exports: [CMSBaseModule],
|
|
26
|
+
providers: [
|
|
27
|
+
...this.createAsyncProvider(options),
|
|
28
|
+
...OptionProviders,
|
|
29
|
+
MemberDataLoader,
|
|
30
|
+
ArticleDataLoader,
|
|
31
|
+
ArticleResolver,
|
|
32
|
+
BackstageArticleResolver,
|
|
33
|
+
ArticleSignatureResolver,
|
|
34
|
+
ArticleQueries,
|
|
35
|
+
ArticleMutations,
|
|
36
|
+
CategoryQueries,
|
|
37
|
+
CategoryMutations,
|
|
38
|
+
BackstageCategoryResolver,
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static forRoot(options: CMSGraphqlBaseModuleOptionsDto): DynamicModule {
|
|
44
|
+
return {
|
|
45
|
+
module: CMSBaseGraphQLModule,
|
|
46
|
+
imports: [CMSBaseModule.forRoot(options)],
|
|
47
|
+
exports: [CMSBaseModule],
|
|
48
|
+
providers: [
|
|
49
|
+
{
|
|
50
|
+
provide: CMS_BASE_GRAPHQL_MODULE_OPTIONS,
|
|
51
|
+
useValue: options,
|
|
52
|
+
},
|
|
53
|
+
...OptionProviders,
|
|
54
|
+
MemberDataLoader,
|
|
55
|
+
ArticleDataLoader,
|
|
56
|
+
ArticleResolver,
|
|
57
|
+
BackstageArticleResolver,
|
|
58
|
+
ArticleSignatureResolver,
|
|
59
|
+
ArticleQueries,
|
|
60
|
+
ArticleMutations,
|
|
61
|
+
CategoryQueries,
|
|
62
|
+
CategoryMutations,
|
|
63
|
+
BackstageCategoryResolver,
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private static createAsyncProvider(options: CMSGraphqlBaseModuleAsyncOptionsDto): Provider[] {
|
|
69
|
+
if (options.useExisting || options.useFactory) {
|
|
70
|
+
return [this.createAsyncOptionsProvider(options)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
this.createAsyncOptionsProvider(options),
|
|
75
|
+
...(options.useClass
|
|
76
|
+
? [
|
|
77
|
+
{
|
|
78
|
+
provide: options.useClass,
|
|
79
|
+
useClass: options.useClass,
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static createAsyncOptionsProvider(options: CMSGraphqlBaseModuleAsyncOptionsDto): Provider {
|
|
87
|
+
if (options.useFactory) {
|
|
88
|
+
return {
|
|
89
|
+
provide: CMS_BASE_GRAPHQL_MODULE_OPTIONS,
|
|
90
|
+
useFactory: options.useFactory,
|
|
91
|
+
inject: options.inject || [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
provide: CMS_BASE_GRAPHQL_MODULE_OPTIONS,
|
|
97
|
+
useFactory: async (optionsFactory: CMSGraphqlBaseModuleOptionFactory) => await optionsFactory.createCMSOptions(),
|
|
98
|
+
inject: [(options.useExisting || options.useClass) as Type<CMSGraphqlBaseModuleOptionFactory>],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export enum BaseAction {
|
|
2
|
+
LIST = 'LIST',
|
|
3
|
+
READ = 'READ',
|
|
4
|
+
CREATE = 'CREATE',
|
|
5
|
+
UPDATE = 'UPDATE',
|
|
6
|
+
DELETE = 'DELETE',
|
|
7
|
+
DELETE_VERSION = 'DELETE_VERSION',
|
|
8
|
+
|
|
9
|
+
SUBMIT = 'SUBMIT',
|
|
10
|
+
PUT_BACK = 'PUT_BACK',
|
|
11
|
+
|
|
12
|
+
APPROVE = 'APPROVE',
|
|
13
|
+
REJECT = 'REJECT',
|
|
14
|
+
|
|
15
|
+
RELEASE = 'RELEASE',
|
|
16
|
+
WITHDRAW = 'WITHDRAW',
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Provider } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
CMS_BASE_GRAPHQL_MODULE_OPTIONS,
|
|
4
|
+
MAP_ARTICLE_CUSTOM_FIELDS_TO_ENTITY_COLUMNS,
|
|
5
|
+
MAP_CATEGORY_CUSTOM_FIELDS_TO_ENTITY_COLUMNS,
|
|
6
|
+
} from '../typings/cms-graphql-base-providers';
|
|
7
|
+
import { CMSGraphqlBaseModuleOptionsDto } from '../typings/cms-graphql-base-root-module-options.dto';
|
|
8
|
+
import { CustomFieldInput } from '../dto/custom-field.input';
|
|
9
|
+
|
|
10
|
+
export const OptionProviders = [
|
|
11
|
+
{
|
|
12
|
+
provide: MAP_ARTICLE_CUSTOM_FIELDS_TO_ENTITY_COLUMNS,
|
|
13
|
+
useFactory: async (
|
|
14
|
+
options?: CMSGraphqlBaseModuleOptionsDto,
|
|
15
|
+
): Promise<
|
|
16
|
+
(customFields: CustomFieldInput[]) => Promise<Record<string, string | object>> | Record<string, string | object>
|
|
17
|
+
> =>
|
|
18
|
+
options?.mapArticleCustomFieldsToEntityColumns
|
|
19
|
+
? options.mapArticleCustomFieldsToEntityColumns
|
|
20
|
+
: (): Record<string, string | object> => ({}),
|
|
21
|
+
inject: [CMS_BASE_GRAPHQL_MODULE_OPTIONS],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
provide: MAP_CATEGORY_CUSTOM_FIELDS_TO_ENTITY_COLUMNS,
|
|
25
|
+
useFactory: async (
|
|
26
|
+
options?: CMSGraphqlBaseModuleOptionsDto,
|
|
27
|
+
): Promise<
|
|
28
|
+
(customFields: CustomFieldInput[]) => Promise<Record<string, string | object>> | Record<string, string | object>
|
|
29
|
+
> =>
|
|
30
|
+
options?.mapCategoryCustomFieldsToEntityColumns
|
|
31
|
+
? options.mapCategoryCustomFieldsToEntityColumns
|
|
32
|
+
: (): Record<string, string | object> => ({}),
|
|
33
|
+
inject: [CMS_BASE_GRAPHQL_MODULE_OPTIONS],
|
|
34
|
+
},
|
|
35
|
+
] as Provider[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import DataLoader from 'dataloader';
|
|
3
|
+
import { Repository } from 'typeorm';
|
|
4
|
+
import { LRUCache } from 'lru-cache';
|
|
5
|
+
import { BaseArticleEntity, DEFAULT_LANGUAGE, RESOLVED_ARTICLE_REPO } from '@rytass/cms-base-nestjs-module';
|
|
6
|
+
import { CategoryDto } from '../dto/category.dto';
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class ArticleDataLoader {
|
|
10
|
+
constructor(
|
|
11
|
+
@Inject(RESOLVED_ARTICLE_REPO)
|
|
12
|
+
private readonly articleRepo: Repository<BaseArticleEntity>,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
private readonly batchLoadCategories = async (
|
|
16
|
+
queryArgs: readonly {
|
|
17
|
+
articleId: string;
|
|
18
|
+
language?: string;
|
|
19
|
+
}[],
|
|
20
|
+
): Promise<CategoryDto[][]> => {
|
|
21
|
+
const qb = this.articleRepo.createQueryBuilder('articles');
|
|
22
|
+
|
|
23
|
+
qb.leftJoinAndSelect('articles.categories', 'categories');
|
|
24
|
+
qb.leftJoinAndSelect('categories.multiLanguageNames', 'multiLanguageNames');
|
|
25
|
+
|
|
26
|
+
qb.andWhere('articles.id IN (:...ids)', {
|
|
27
|
+
ids: queryArgs.map(arg => arg.articleId),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const articles = await qb.getMany();
|
|
31
|
+
|
|
32
|
+
const categoryMap = articles.reduce<Record<string, CategoryDto[]>>(
|
|
33
|
+
(vars, article) =>
|
|
34
|
+
article.categories.reduce(
|
|
35
|
+
(cVars, category) =>
|
|
36
|
+
category.multiLanguageNames.reduce(
|
|
37
|
+
(mVars, multiLanguageName) => ({
|
|
38
|
+
...mVars,
|
|
39
|
+
[`${article.id}:${multiLanguageName.language}`]: [
|
|
40
|
+
...(mVars[`${article.id}:${multiLanguageName.language}`] || []),
|
|
41
|
+
{
|
|
42
|
+
...category,
|
|
43
|
+
...multiLanguageName,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}),
|
|
47
|
+
cVars,
|
|
48
|
+
),
|
|
49
|
+
vars,
|
|
50
|
+
),
|
|
51
|
+
{},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return queryArgs.map(arg => categoryMap[`${arg.articleId}:${arg.language ?? DEFAULT_LANGUAGE}`] ?? []);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
readonly categoriesLoader = new DataLoader<
|
|
58
|
+
{
|
|
59
|
+
articleId: string;
|
|
60
|
+
language?: string;
|
|
61
|
+
},
|
|
62
|
+
CategoryDto[],
|
|
63
|
+
string
|
|
64
|
+
>(this.batchLoadCategories, {
|
|
65
|
+
cache: true,
|
|
66
|
+
cacheMap: new LRUCache<string, Promise<CategoryDto[]>>({
|
|
67
|
+
ttl: 1000 * 60, // 1 minute
|
|
68
|
+
ttlAutopurge: true,
|
|
69
|
+
max: 1000,
|
|
70
|
+
}),
|
|
71
|
+
cacheKeyFn: (queryArgs): string => `${queryArgs.articleId}:${queryArgs.language ?? DEFAULT_LANGUAGE}`,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
readonly categoriesLoaderNoCache = new DataLoader<
|
|
75
|
+
{
|
|
76
|
+
articleId: string;
|
|
77
|
+
language?: string;
|
|
78
|
+
},
|
|
79
|
+
CategoryDto[],
|
|
80
|
+
string
|
|
81
|
+
>(this.batchLoadCategories, {
|
|
82
|
+
cache: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { BaseMemberEntity, RESOLVED_MEMBER_REPO } from '@rytass/member-base-nestjs-module';
|
|
3
|
+
import DataLoader from 'dataloader';
|
|
4
|
+
import { LRUCache } from 'lru-cache';
|
|
5
|
+
import { Repository } from 'typeorm';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class MemberDataLoader {
|
|
9
|
+
constructor(
|
|
10
|
+
@Inject(RESOLVED_MEMBER_REPO)
|
|
11
|
+
private readonly memberRepo: Repository<BaseMemberEntity>,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
readonly loader = new DataLoader<string, BaseMemberEntity | null>(
|
|
15
|
+
async (ids): Promise<(BaseMemberEntity | null)[]> => {
|
|
16
|
+
const qb = this.memberRepo.createQueryBuilder('members');
|
|
17
|
+
|
|
18
|
+
qb.withDeleted();
|
|
19
|
+
qb.andWhere('members.id IN (:...ids)', { ids });
|
|
20
|
+
|
|
21
|
+
const users = await qb.getMany();
|
|
22
|
+
|
|
23
|
+
const userMap = new Map(users.map(user => [user.id, user]));
|
|
24
|
+
|
|
25
|
+
return ids.map(id => userMap.get(id) ?? null);
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
cache: true,
|
|
29
|
+
cacheMap: new LRUCache<string, Promise<BaseMemberEntity | null>>({
|
|
30
|
+
ttl: 1000 * 60 * 10, // 10 minutes
|
|
31
|
+
ttlAutopurge: true,
|
|
32
|
+
max: 1000,
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|