import JSON5 from 'json5'; import { isObject, merge, omit, trim } from 'lodash'; import outmatch from 'outmatch'; import { PlainObject } from 'simplytyped'; import { GeneratorConfiguration } from '../types'; export type ObjectSetting = { /** * Act as named import or namespaceImport or defaultImport */ name: string; kind: 'Decorator' | 'Field' | 'FieldType' | 'PropertyType' | 'ObjectType'; arguments?: string[] | Record<string, unknown>; input: boolean; output: boolean; model: boolean; match?: (test: string) => boolean; from: string; namespace?: string; defaultImport?: string | true; namespaceImport?: string; namedImport?: boolean; }; interface ObjectSettingsFilterArgs { name: string; input?: boolean; output?: boolean; } export class ObjectSettings extends Array<ObjectSetting> { shouldHideField({ name, input = false, output = false, }: ObjectSettingsFilterArgs): boolean { const hideField = this.find(s => s.name === 'HideField'); return Boolean( (hideField?.input && input) || (hideField?.output && output) || hideField?.match?.(name), ); } getFieldType({ name, input, output, }: ObjectSettingsFilterArgs): ObjectSetting | undefined { const fieldType = this.find(s => s.kind === 'FieldType'); if (!fieldType) { return undefined; } if (fieldType.match) { // eslint-disable-next-line unicorn/prefer-regexp-test return fieldType.match(name) ? fieldType : undefined; } if (input && !fieldType.input) { return undefined; } if (output && !fieldType.output) { return undefined; } return fieldType; } getPropertyType({ name, input, output, }: ObjectSettingsFilterArgs): ObjectSetting | undefined { const propertyType = this.find(s => s.kind === 'PropertyType'); if (!propertyType) { return undefined; } if (propertyType.match) { // eslint-disable-next-line unicorn/prefer-regexp-test return propertyType.match(name) ? propertyType : undefined; } if (input && !propertyType.input) { return undefined; } if (output && !propertyType.output) { return undefined; } return propertyType; } getObjectTypeArguments(options: Record<string, any>): string[] { const objectTypeOptions = merge({}, options); const resultArguments: any[] = [objectTypeOptions]; const objectType = this.find(s => s.kind === 'ObjectType'); if (objectType && isObject(objectType.arguments)) { const name = (objectType.arguments as PlainObject).name; merge(objectTypeOptions, omit(objectType.arguments, 'name')); if (name) { resultArguments.unshift(name); } } return resultArguments.map(x => JSON5.stringify(x)); } fieldArguments(): Record<string, unknown> | undefined { const item = this.find(item => item.kind === 'Field'); if (item) { return item.arguments as Record<string, unknown>; } } } export function createObjectSettings(args: { text: string; config: GeneratorConfiguration; }) { const { config, text } = args; const result = new ObjectSettings(); const textLines = text.split('\n'); const documentationLines: string[] = []; let fieldElement = result.find(item => item.kind === 'Field'); if (!fieldElement) { fieldElement = { name: '', kind: 'Field', arguments: {}, } as ObjectSetting; } for (const line of textLines) { const match = /^@(?<name>\w+(\.(\w+))?)\((?<args>.*)\)/.exec(line); const { element, documentLine } = createSettingElement({ line, config, fieldElement, match, }); if (element) { result.push(element); } if (documentLine) { documentationLines.push(line); } } return { settings: result, documentation: documentationLines.filter(Boolean).join('\n') || undefined, }; } function createSettingElement({ line, config, fieldElement, match, }: { line: string; config: GeneratorConfiguration; fieldElement: ObjectSetting; match: RegExpExecArray | null; }) { const result = { documentLine: '', element: undefined as ObjectSetting | undefined, }; if (line.startsWith('@deprecated')) { fieldElement.arguments!['deprecationReason'] = trim(line.slice(11)); result.element = fieldElement; return result; } const name = match?.groups?.name; if (!(match && name)) { result.documentLine = line; return result; } const element: ObjectSetting = { kind: 'Decorator', name: '', arguments: [], input: false, output: false, model: false, from: '', }; result.element = element; if (name === 'TypeGraphQL.omit' || name === 'HideField') { Object.assign(element, hideFieldDecorator(match)); return result; } if (['FieldType', 'PropertyType'].includes(name) && match.groups?.args) { const options = customType(match.groups.args); merge(element, options.namespace && config.fields[options.namespace], options, { kind: name, }); return result; } if (name === 'ObjectType' && match.groups?.args) { element.kind = 'ObjectType'; const options = customType(match.groups.args) as Record<string, unknown>; if (typeof options[0] === 'string' && options[0]) { options.name = options[0]; } if (isObject(options[1])) { merge(options, options[1]); } element.arguments = { name: options.name, isAbstract: options.isAbstract, }; return result; } if (name === 'Directive' && match.groups?.args) { const options = customType(match.groups.args); merge(element, { model: true, from: '@nestjs/graphql' }, options, { name, namespace: false, kind: 'Decorator', arguments: Array.isArray(options.arguments) ? options.arguments.map(s => JSON5.stringify(s)) : options.arguments, }); return result; } const namespace = getNamespace(name); element.namespaceImport = namespace; const options = { name, arguments: (match.groups?.args || '') .split(',') .map(s => trim(s)) .filter(Boolean), }; merge(element, namespace && config.fields[namespace], options); return result; } function customType(args: string) { const result: Partial<ObjectSetting> = {}; let options = parseArgs(args); if (typeof options === 'string') { options = { name: options }; } Object.assign(result, options); const namespace = getNamespace(options.name); result.namespace = namespace; if ((options as { name: string | undefined }).name?.includes('.')) { result.namespaceImport = namespace; } if (typeof options.match === 'string' || Array.isArray(options.match)) { result.match = outmatch(options.match, { separator: false }); } return result; } function hideFieldDecorator(match: RegExpExecArray) { const result: Partial<ObjectSetting> = { name: 'HideField', arguments: [], from: '@nestjs/graphql', defaultImport: undefined, namespaceImport: undefined, match: undefined, }; if (!match.groups?.args) { result.output = true; return result; } if (match.groups.args.includes('{') && match.groups.args.includes('}')) { const options = parseArgs(match.groups.args) as Record<string, unknown>; result.output = Boolean(options.output); result.input = Boolean(options.input); if (typeof options.match === 'string' || Array.isArray(options.match)) { result.match = outmatch(options.match, { separator: false }); } } else { if (/output:\s*true/.test(match.groups.args)) { result.output = true; } if (/input:\s*true/.test(match.groups.args)) { result.input = true; } } return result; } function parseArgs(string: string): Record<string, unknown> | string { try { return JSON5.parse(string); } catch { try { return JSON5.parse(`[${string}]`); } catch { throw new Error(`Failed to parse: ${string}`); } } } function getNamespace(name: unknown): string | undefined { if (name === undefined) { return undefined; } let result = String(name); if (result.includes('.')) { [result] = result.split('.'); } // eslint-disable-next-line consistent-return return result; }