@axinom/mosaic-graphql-codegen-plugins 0.2.0-rc.0

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 ADDED
@@ -0,0 +1,16 @@
1
+ # @axinom/mosaic-graphql-codegen-plugins
2
+
3
+ ## About the Package
4
+
5
+ This package is part of the Axinom Mosaic development platform. More information
6
+ can be found at https://portal.axinom.com/mosaic.
7
+
8
+ ## License
9
+
10
+ This package can be licensed under the
11
+ [Axinom Products Licensing Agreement](https://portal.axinom.com/mosaic/contracts/products-licensing-agreement)
12
+ or evaluated under the
13
+ [Axinom Products Evaluation Agreement](https://portal.axinom.com/mosaic/contracts/products-evaluation-agreement).
14
+ No part of Axinom's software may be copied, modified, propagated, or distributed
15
+ except in accordance with the terms contained in the Axinom Products Licensing
16
+ Agreement and Axinom Products Evaluation Agreement.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@axinom/mosaic-graphql-codegen-plugins",
3
+ "version": "0.2.0-rc.0",
4
+ "description": "Library of graphql-codegen plugins for Mosaic workflows",
5
+ "scripts": {
6
+ "clean": "rimraf dist",
7
+ "build": "yarn clean && tsc --project tsconfig.build.json",
8
+ "test": "jest --silent",
9
+ "lint": "eslint . --ext .ts,.tsx,.js --color --cache"
10
+ },
11
+ "author": "Axinom",
12
+ "license": "PROPRIETARY",
13
+ "keywords": [
14
+ "axinom",
15
+ "mosaic",
16
+ "axinom mosaic"
17
+ ],
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "exports": {
23
+ "./generate-enum-comments": "./dist/generate-enum-comments/generate-enum-comments.js",
24
+ "./generate-bulk-edit-ui-config": "./dist/generate-bulk-edit-ui-config/generate-bulk-edit-ui-config.js"
25
+ },
26
+ "dependencies": {
27
+ "change-case-all": "^2.1.0",
28
+ "handlebars": "^4.7.7"
29
+ },
30
+ "devDependencies": {
31
+ "@graphql-codegen/plugin-helpers": "^5.1.1",
32
+ "jest": "^29",
33
+ "rimraf": "^3.0.2",
34
+ "ts-node": "^10.9.1",
35
+ "typescript": "^5.0.4"
36
+ },
37
+ "peerDependencies": {
38
+ "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "gitHead": "4d2dbe509438f1c3220e7bbc4aaba013da327d67"
44
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ GraphQLInputObjectType,
3
+ GraphQLObjectType,
4
+ GraphQLSchema,
5
+ GraphQLString,
6
+ } from 'graphql';
7
+ import { BulkEditPluginConfig, plugin } from './generate-bulk-edit-ui-config';
8
+
9
+ describe('generate-bulk-edit-ui-config plugin', () => {
10
+ let schema: GraphQLSchema;
11
+
12
+ beforeEach(() => {
13
+ const RelatedEntitiesInputType = new GraphQLInputObjectType({
14
+ name: 'RelatedEntitiesInput',
15
+ fields: {
16
+ id: { type: GraphQLString },
17
+ name: { type: GraphQLString },
18
+ },
19
+ });
20
+
21
+ const SetInputType = new GraphQLInputObjectType({
22
+ name: 'SetInput',
23
+ fields: {
24
+ status: { type: GraphQLString },
25
+ },
26
+ });
27
+
28
+ const MutationType = new GraphQLObjectType({
29
+ name: 'Mutation',
30
+ fields: {
31
+ bulkEditEntity: {
32
+ type: GraphQLString,
33
+ args: {
34
+ relatedEntitiesToAdd: { type: RelatedEntitiesInputType },
35
+ relatedEntitiesToRemove: { type: RelatedEntitiesInputType },
36
+ set: { type: SetInputType },
37
+ },
38
+ },
39
+ anotherMutation: {
40
+ type: GraphQLString,
41
+ args: {
42
+ set: { type: SetInputType },
43
+ },
44
+ },
45
+ },
46
+ });
47
+
48
+ const QueryType = new GraphQLObjectType({
49
+ name: 'Query',
50
+ fields: {
51
+ dummy: { type: GraphQLString },
52
+ },
53
+ });
54
+
55
+ schema = new GraphQLSchema({
56
+ query: QueryType,
57
+ mutation: MutationType,
58
+ types: [RelatedEntitiesInputType, SetInputType],
59
+ });
60
+ });
61
+
62
+ it('should generate config for mutations with relatedEntitiesToAdd, relatedEntitiesToRemove, and set', () => {
63
+ const result = plugin(schema, [], {});
64
+ expect(result).toContain('BulkEditEntityFormFieldsConfig');
65
+ expect(result).toContain('relatedEntitiesToAdd');
66
+ expect(result).toContain('relatedEntitiesToRemove');
67
+ expect(result).toContain('set');
68
+ expect(result).toContain('fields');
69
+ expect(result).toContain('mutation');
70
+ });
71
+
72
+ it('should generate config for mutations with only set argument', () => {
73
+ const result = plugin(schema, [], {});
74
+ expect(result).toContain('AnotherMutationFormFieldsConfig');
75
+ expect(result).toContain('set');
76
+ expect(result).toContain('fields');
77
+ });
78
+
79
+ it('should use custom keys from config', () => {
80
+ const customConfig: Partial<BulkEditPluginConfig> = {
81
+ addKey: 'customAdd',
82
+ removeKey: 'customRemove',
83
+ setKey: 'customSet',
84
+ filterKey: 'customFilter',
85
+ };
86
+ const result = plugin(schema, [], customConfig);
87
+ expect(result).toContain('customAdd');
88
+ expect(result).toContain('customRemove');
89
+ expect(result).toContain('customSet');
90
+ expect(result).toContain('customFilter');
91
+ });
92
+
93
+ it('should return empty string if mutationType or queryType is missing', () => {
94
+ const schemaWithoutMutation = new GraphQLSchema({
95
+ query: new GraphQLObjectType({
96
+ name: 'Query',
97
+ fields: { dummy: { type: GraphQLString } },
98
+ }),
99
+ });
100
+ const result = plugin(schemaWithoutMutation, [], {});
101
+ expect(result).toBe('');
102
+ });
103
+
104
+ it('should not generate config if mutation has no relevant args', () => {
105
+ const MutationType = new GraphQLObjectType({
106
+ name: 'Mutation',
107
+ fields: {
108
+ noRelevantArgs: {
109
+ type: GraphQLString,
110
+ args: {
111
+ irrelevant: { type: GraphQLString },
112
+ },
113
+ },
114
+ },
115
+ });
116
+ const schemaWithIrrelevantMutation = new GraphQLSchema({
117
+ query: new GraphQLObjectType({
118
+ name: 'Query',
119
+ fields: { dummy: { type: GraphQLString } },
120
+ }),
121
+ mutation: MutationType,
122
+ });
123
+ const result = plugin(schemaWithIrrelevantMutation, [], {});
124
+ expect(result).not.toContain('NoRelevantArgsFormFieldsConfig');
125
+ });
126
+ });
@@ -0,0 +1,134 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { PluginFunction } from '@graphql-codegen/plugin-helpers';
3
+ import { capitalCase, pascalCase } from 'change-case-all';
4
+ import { GraphQLFieldMap, GraphQLList, GraphQLObjectType } from 'graphql';
5
+
6
+ export interface BulkEditPluginConfig {
7
+ addKey?: string;
8
+ removeKey?: string;
9
+ setKey?: string;
10
+ filterKey?: string;
11
+ }
12
+
13
+ interface Data {
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export const plugin: PluginFunction<Partial<BulkEditPluginConfig>> = (
18
+ schema,
19
+ _documents,
20
+ config,
21
+ ) => {
22
+ const addKey = config.addKey || 'relatedEntitiesToAdd';
23
+ const removeKey = config.removeKey || 'relatedEntitiesToRemove';
24
+ const setKey = config.setKey || 'set';
25
+ const filterKey = config.filterKey || 'filter';
26
+
27
+ const mutationType = schema.getMutationType();
28
+ const queryType = schema.getQueryType();
29
+
30
+ if (!mutationType || !queryType) {
31
+ // eslint-disable-next-line no-console
32
+ console.warn('No mutation type or query type found in schema');
33
+ return '';
34
+ }
35
+
36
+ const typesMap = schema.getTypeMap();
37
+
38
+ const mutations = mutationType.getFields();
39
+ const configs = Object.keys(mutations).map((mutationName) => {
40
+ const mutation = mutations[mutationName];
41
+
42
+ let formFieldsConfig = {};
43
+
44
+ mutation.args.map((arg) => {
45
+ switch (arg.name.toString()) {
46
+ case 'relatedEntitiesToAdd':
47
+ formFieldsConfig = {
48
+ ...formFieldsConfig,
49
+ ...resolveFields(
50
+ (typesMap[arg.type.toString()] as GraphQLObjectType).getFields(),
51
+ addKey,
52
+ 'Add',
53
+ ),
54
+ };
55
+ break;
56
+ case 'relatedEntitiesToRemove':
57
+ formFieldsConfig = {
58
+ ...formFieldsConfig,
59
+ ...resolveFields(
60
+ (typesMap[arg.type.toString()] as GraphQLObjectType).getFields(),
61
+ removeKey,
62
+ 'Remove',
63
+ ),
64
+ };
65
+ break;
66
+ case 'set':
67
+ formFieldsConfig = {
68
+ ...formFieldsConfig,
69
+ ...resolveFields(
70
+ (typesMap[arg.type.toString()] as GraphQLObjectType).getFields(),
71
+ setKey,
72
+ ),
73
+ };
74
+ break;
75
+ }
76
+ });
77
+
78
+ if (Object.keys(formFieldsConfig).length > 0) {
79
+ return `export const ${pascalCase(
80
+ mutationName,
81
+ )}FormFieldsConfig = { mutation: '${mutationName}', keys: { add: '${addKey}', remove: '${removeKey}', set: '${setKey}', filter: '${filterKey}' }, fields: ${JSON.stringify(
82
+ formFieldsConfig,
83
+ null,
84
+ 2,
85
+ )}};`;
86
+ }
87
+ });
88
+
89
+ return ['/** Bulk Edit Configurations **/', ...configs]
90
+ .filter((config) => config !== null && config !== undefined)
91
+ .join('\n');
92
+ };
93
+
94
+ function resolveFields(
95
+ fields: GraphQLFieldMap<any, any>,
96
+ action: string,
97
+ postfix = '',
98
+ ): Data {
99
+ const resultingFields: Data = {};
100
+ Object.keys(fields).map((fieldName) => {
101
+ const field = fields[fieldName];
102
+
103
+ let type: string | Data[] = field.type.toString();
104
+
105
+ if (Object.keys(field.type).includes('ofType')) {
106
+ const compositeType: Data[] = [];
107
+
108
+ Object.keys((field.type as GraphQLList<any>).ofType.getFields()).forEach(
109
+ (key) => {
110
+ const name = (field.type as GraphQLList<any>).ofType.getFields()[key]
111
+ .name;
112
+ const type = (field.type as GraphQLList<any>).ofType.getFields()[key]
113
+ .type;
114
+ compositeType.push({
115
+ [name]: type,
116
+ });
117
+ },
118
+ );
119
+
120
+ type = compositeType;
121
+ }
122
+
123
+ resultingFields[postfix ? `${fieldName}${postfix}` : fieldName] = {
124
+ type: type,
125
+ label: postfix
126
+ ? `${capitalCase(fieldName)} (${postfix})`
127
+ : capitalCase(fieldName),
128
+ originalFieldName: fieldName,
129
+ action: action,
130
+ };
131
+ });
132
+
133
+ return resultingFields;
134
+ }
@@ -0,0 +1,185 @@
1
+ import { buildSchema } from 'graphql';
2
+ import { plugin } from './generate-enum-comments';
3
+
4
+ describe('generate-enum-comments plugin', () => {
5
+ describe('plugin function', () => {
6
+ it('should generate enum labels for enums with descriptions', () => {
7
+ const schema = buildSchema(`
8
+ enum Status {
9
+ """Active status"""
10
+ ACTIVE
11
+ """Inactive status"""
12
+ INACTIVE
13
+ """Pending status"""
14
+ PENDING
15
+ }
16
+ `);
17
+
18
+ const result = plugin(schema, [], {}, { outputFile: '' });
19
+
20
+ expect(result).toContain('export const StatusLabel');
21
+ expect(result).toContain("'ACTIVE' : 'Active status'");
22
+ expect(result).toContain("'INACTIVE' : 'Inactive status'");
23
+ expect(result).toContain("'PENDING' : 'Pending status'");
24
+ });
25
+
26
+ it('should handle enums with mixed descriptions', () => {
27
+ const schema = buildSchema(`
28
+ enum Priority {
29
+ """High priority task"""
30
+ HIGH
31
+ MEDIUM
32
+ """Low priority task"""
33
+ LOW
34
+ }
35
+ `);
36
+
37
+ const result = plugin(schema, [], {}, { outputFile: '' });
38
+
39
+ expect(result).toContain('export const PriorityLabel');
40
+ expect(result).toContain("'HIGH' : 'High priority task'");
41
+ expect(result).not.toContain("'MEDIUM' :");
42
+ expect(result).toContain("'LOW' : 'Low priority task'");
43
+ });
44
+
45
+ it('should handle enums without descriptions', () => {
46
+ const schema = buildSchema(`
47
+ enum Color {
48
+ RED
49
+ GREEN
50
+ BLUE
51
+ }
52
+ `);
53
+
54
+ const result = plugin(schema, [], {}, { outputFile: '' });
55
+
56
+ expect(result).toContain('export const ColorLabel');
57
+ expect(result).not.toContain("'RED' :");
58
+ expect(result).not.toContain("'GREEN' :");
59
+ expect(result).not.toContain("'BLUE' :");
60
+ });
61
+
62
+ it('should handle multiple enums', () => {
63
+ const schema = buildSchema(`
64
+ enum Status {
65
+ """Active"""
66
+ ACTIVE
67
+ """Inactive"""
68
+ INACTIVE
69
+ }
70
+
71
+ enum Role {
72
+ """Administrator role"""
73
+ ADMIN
74
+ """User role"""
75
+ USER
76
+ }
77
+ `);
78
+
79
+ const result = plugin(schema, [], {}, { outputFile: '' });
80
+
81
+ expect(result).toContain('export const StatusLabel');
82
+ expect(result).toContain('export const RoleLabel');
83
+ expect(result).toContain("'ACTIVE' : 'Active'");
84
+ expect(result).toContain("'ADMIN' : 'Administrator role'");
85
+ });
86
+
87
+ it('should filter out internal GraphQL types', () => {
88
+ const schema = buildSchema(`
89
+ enum Status {
90
+ ACTIVE
91
+ INACTIVE
92
+ }
93
+ `);
94
+
95
+ const result = plugin(schema, [], {}, { outputFile: '' });
96
+
97
+ expect(result).toContain('export const StatusLabel');
98
+ expect(result).not.toContain('__Schema');
99
+ expect(result).not.toContain('__Type');
100
+ expect(result).not.toContain('__Directive');
101
+ });
102
+
103
+ it('should handle descriptions with quotes and whitespace', () => {
104
+ const schema = buildSchema(`
105
+ enum Status {
106
+ """This is a 'quoted' description with multiple spaces"""
107
+ ACTIVE
108
+ }
109
+ `);
110
+
111
+ const result = plugin(schema, [], {}, { outputFile: '' });
112
+
113
+ expect(result).toContain(
114
+ "'ACTIVE' : 'This is a \\'quoted\\' description with multiple spaces'",
115
+ );
116
+ });
117
+
118
+ it('should generate proxy handler code', () => {
119
+ const schema = buildSchema(`
120
+ enum Status {
121
+ ACTIVE
122
+ }
123
+ `);
124
+
125
+ const result = plugin(schema, [], {}, { outputFile: '' });
126
+
127
+ expect(result).toContain('const getWithFallbackHandler');
128
+ expect(result).toContain('ProxyHandler');
129
+ expect(result).toContain('new Proxy');
130
+ expect(result).toContain('getWithFallbackHandler');
131
+ });
132
+
133
+ it('should handle empty schema', () => {
134
+ const schema = buildSchema(`
135
+ type Query {
136
+ hello: String
137
+ }
138
+ `);
139
+
140
+ const result = plugin(schema, [], {}, { outputFile: '' });
141
+
142
+ expect(result).toContain('/** generate-enum-comments **/');
143
+ expect(result).toContain('const getWithFallbackHandler');
144
+ expect(result).not.toContain('export const');
145
+ });
146
+
147
+ it('should handle schema with only scalar and object types', () => {
148
+ const schema = buildSchema(`
149
+ type User {
150
+ id: ID!
151
+ name: String!
152
+ }
153
+
154
+ type Query {
155
+ user: User
156
+ }
157
+ `);
158
+
159
+ const result = plugin(schema, [], {}, { outputFile: '' });
160
+
161
+ expect(result).toContain('/** generate-enum-comments **/');
162
+ expect(result).not.toContain('export const');
163
+ });
164
+
165
+ it('should generate valid TypeScript code structure', () => {
166
+ const schema = buildSchema(`
167
+ enum Status {
168
+ """Active status"""
169
+ ACTIVE
170
+ }
171
+ `);
172
+
173
+ const result = plugin(schema, [], {}, { outputFile: '' });
174
+
175
+ // Check for proper TypeScript syntax
176
+ expect(result).toMatch(
177
+ /export const \w+Label = new Proxy<Record<string,string>>/,
178
+ );
179
+ expect(result).toContain(
180
+ 'ProxyHandler<{ [key: string | symbol]: string}>',
181
+ );
182
+ expect(result).toContain('target.hasOwnProperty(name)');
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,51 @@
1
+ import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers';
2
+ import { GraphQLNamedType, GraphQLSchema } from 'graphql';
3
+ import handlebars from 'handlebars';
4
+
5
+ handlebars.registerHelper('jsonSafe', (/** @type {String} */ value) => {
6
+ return value.replace(/'/g, "\\'").replace(/\s+/g, ' ');
7
+ });
8
+
9
+ const template = handlebars.compile(`
10
+ /** generate-enum-comments **/
11
+ const getWithFallbackHandler: ProxyHandler<{ [key: string | symbol]: string}> = {
12
+ get: function(target, name) {
13
+ return target.hasOwnProperty(name) ? target[name] : name;
14
+ }
15
+ };
16
+ {{#each enums}}
17
+
18
+ export const {{{name}}}Label = new Proxy<Record<string,string>>({
19
+ {{#each _values}}
20
+ {{#if description}}
21
+ '{{{name}}}' : '{{{jsonSafe description}}}',
22
+ {{/if}}
23
+ {{/each}}
24
+ }, getWithFallbackHandler);
25
+ {{/each}}
26
+ `);
27
+
28
+ function getEnumTypeMap(schema: GraphQLSchema): GraphQLNamedType[] {
29
+ const typeMap = schema.getTypeMap();
30
+ const result: GraphQLNamedType[] = [];
31
+ for (const typeName in typeMap) {
32
+ const type = typeMap[typeName];
33
+ if (
34
+ type.constructor.name === 'GraphQLEnumType' &&
35
+ !typeName.startsWith('__') // Filter out internal types
36
+ ) {
37
+ result.push(type);
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ export const plugin: PluginFunction<Types.ConfiguredOutput> = (schema) => {
44
+ const enums = getEnumTypeMap(schema);
45
+
46
+ const result = template({
47
+ enums,
48
+ });
49
+
50
+ return result;
51
+ };