├── .npmignore ├── .release-it.json ├── .gitignore ├── src ├── constants.ts ├── helpers │ ├── index.ts │ ├── to-camel-case.ts │ ├── get-inner-type.ts │ ├── flatten-array.ts │ ├── provide-dynamic-resolvers.ts │ ├── pluralize.ts │ └── extract-grapql-selections.ts ├── index.ts ├── use-dynamic-resolvers.ts ├── dynamic-resolver │ ├── dynamic-resolver-base.ts │ ├── event-params.interface.ts │ ├── dynamic-resolver-ops.ts │ └── make-dynamic-resolver.ts ├── types.ts ├── navigation-property.ts └── dynamic-navigations.ts ├── tsconfig.json ├── package.json ├── CHANGELOG.md ├── README.md └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | *lock.y[a]ml 2 | release.sh 3 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *lock.y[a]ml 3 | .DS_Store 4 | dist 5 | release.sh 6 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A key that is used to inject a prisma client service. 3 | */ 4 | export const SYM_PRISMA_CLIENT = Symbol('[[PrismaClient]]') 5 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './extract-grapql-selections' 2 | export * from './provide-dynamic-resolvers' 3 | export * from './to-camel-case' 4 | export * from './pluralize' 5 | -------------------------------------------------------------------------------- /src/helpers/to-camel-case.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates another string where the first letter of it is uncapitalized. 3 | * 4 | * @export 5 | * @param {string} value The input string. 6 | * @return {string} A string which is camel-cased. 7 | */ 8 | export function toCamelCase(value: string) { 9 | return value.replace(/\b(\p{Alpha})(.*?)\b/u, (_string, match, rest) => { 10 | return match.toLowerCase() + rest 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dynamic-navigations' 2 | 3 | export { 4 | extractGraphQLSelectionPath, 5 | extractGraphQLSelections, 6 | provideDynamicResolvers, 7 | getGraphQLSelectionsObject, 8 | modifyGraphQLSelections, 9 | IGraphQLExtractSelectionMap, 10 | IGraphQLPrismaSelect, 11 | } from './helpers' 12 | 13 | export { NavigationProperty } from './navigation-property' 14 | export { UseDynamicResolvers } from './use-dynamic-resolvers' 15 | -------------------------------------------------------------------------------- /src/helpers/get-inner-type.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLType } from 'graphql' 2 | 3 | /** 4 | * Gets the actual GraphQLType that is wrapped by list or non-nullable 5 | * types. 6 | * 7 | * @param {GraphQLType} input The input. 8 | * @return {GraphQLType} The wrapped object type. 9 | */ 10 | export function getInnerType(input: GraphQLType): GraphQLType { 11 | if ('ofType' in input) { 12 | return getInnerType(input.ofType) 13 | } 14 | 15 | return input 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/flatten-array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a generator which flattens a given array. 3 | * 4 | * @export 5 | * @template T The type of the items. 6 | * @param {T[]} array The array that will be flattened. 7 | * @param {number} [depth=Infinity] The depth of the flattening. 8 | * @return {Generator} The generator instance of the given type. 9 | */ 10 | export function* flattenArray(array: T[], depth: number = Infinity): Generator { 11 | if (depth === undefined) { 12 | depth = 1 13 | } 14 | for (const item of array) { 15 | if (Array.isArray(item) && depth > 0) { 16 | yield* flattenArray(item, depth - 1) 17 | } else { 18 | yield item 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "module": "commonjs", 7 | "rootDir": "./src", 8 | "moduleResolution": "node", 9 | "baseUrl": ".", 10 | "declaration": true, 11 | "emitDeclarationOnly": false, 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "newLine": "lf", 15 | "esModuleInterop": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "strict": true, 18 | "skipDefaultLibCheck": true, 19 | "skipLibCheck": true, 20 | "lib": [ 21 | "ES2015" 22 | ] 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "tests", 27 | "dist" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/use-dynamic-resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common' 2 | 3 | import { 4 | IUseDynamicResolversParams, 5 | registerDynamicResolver, 6 | } from './dynamic-resolver/dynamic-resolver-ops' 7 | 8 | /** 9 | * Decorates a class by creating dynamic resolvers from the properties 10 | * decorated with {@link NavigationProperty}. 11 | * 12 | * @export 13 | * @param {IUseDynamicResolversParams} [params] The parameters for resolvers. 14 | * @return {ClassDecorator} A {@link ClassDecorator}. 15 | */ 16 | export function UseDynamicResolvers(params: IUseDynamicResolversParams = {}) { 17 | return function _UseDynamicResolvers(sourceClass: Type) { 18 | registerDynamicResolver({ 19 | ...params, 20 | target: sourceClass, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/dynamic-resolver/dynamic-resolver-base.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLResolveInfo } from 'graphql' 2 | import type { INavigationMap } from '../dynamic-navigations' 3 | import type { Dictionary } from '../types' 4 | 5 | export abstract class DynamicResolverBase { 6 | constructor(protected readonly prisma: any) {} 7 | 8 | protected resolve(parent: any, _primaryKeyName: string, _info: GraphQLResolveInfo, navigationMap: INavigationMap, _selectionMap?: Dictionary) { 9 | const { 10 | relation, 11 | sourceProperty, 12 | sourceTableName, 13 | targetTableName, 14 | } = navigationMap 15 | 16 | const [ _left, right ] = relation.split(':') 17 | const isArray = right.indexOf('*') >= 0 18 | 19 | // The `parent` is the loaded instance 20 | // If the source we are looking for is loaded, then process it. 21 | if (sourceProperty in parent) { 22 | const data = (parent as any)[ sourceProperty ] 23 | if (isArray) { 24 | return data.map((it: any) => it[ targetTableName ]) 25 | } 26 | else { 27 | return data 28 | } 29 | } 30 | else { 31 | throw new Error(`Could not resolve from ${sourceTableName}: ${sourceProperty}`) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/helpers/provide-dynamic-resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common' 2 | import { PrismaClient } from '@prisma/client' 3 | 4 | import { SYM_PRISMA_CLIENT } from '../constants' 5 | import { 6 | generateDynamicResolvers, 7 | } from '../dynamic-resolver/make-dynamic-resolver' 8 | import { flattenArray } from './flatten-array' 9 | 10 | /** 11 | * Returns the {@link Type} array of dynamic resolvers. 12 | * 13 | * @export 14 | * @param {Type} prismaService The prisma service used for 15 | * reaching the database by the resolvers. 16 | * 17 | * @param {string} [moduleName='_global'] The name of the module which will be 18 | * loaded. 19 | * 20 | * @return {Type[]} The array of dynamic resolvers. 21 | */ 22 | export function provideDynamicResolvers(prismaService: Type, moduleName = '_global'): Type[] { 23 | return [ 24 | { provide: SYM_PRISMA_CLIENT, useExisting: prismaService } as any, 25 | ...buildDynamicResolversArray(moduleName) 26 | ] 27 | } 28 | 29 | function buildDynamicResolversArray(moduleName: string) { 30 | const resolversByClasses = generateDynamicResolvers(moduleName) 31 | const resolvers: Type[] = [] 32 | for (const className in resolversByClasses) { 33 | const classResolvers = resolversByClasses[ className ] 34 | resolvers.push(...flattenArray(classResolvers)) 35 | } 36 | 37 | return resolvers 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-prisma-dynamic-resolvers", 3 | "version": "0.1.17", 4 | "description": "A library containing decorators for creating dynamic resolvers between database table objects through their navigation property.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "jest", 9 | "prepublish": "tsc", 10 | "build": "tsc", 11 | "build-watch": "tsc --watch", 12 | "release": "release-it" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/incetarik/nestjs-prisma-dynamic-resolvers.git" 17 | }, 18 | "keywords": [ 19 | "nestjs", 20 | "prisma", 21 | "resolver", 22 | "decorator" 23 | ], 24 | "author": "Tarık İnce ", 25 | "license": "SEE LICENSE IN LICENSE", 26 | "bugs": { 27 | "url": "https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/issues" 28 | }, 29 | "homepage": "https://github.com/incetarik/nestjs-prisma-dynamic-resolvers#readme", 30 | "peerDependencies": { 31 | "@prisma/client": ">= 4.2", 32 | "@nestjs/common": ">= 8.4", 33 | "reflect-metadata": ">= 0.1", 34 | "rxjs": ">= 7.5", 35 | "@nestjs/graphql": ">= 10.0", 36 | "@nestjs/core": ">= 8.4", 37 | "graphql": ">= 16.6", 38 | "prisma": ">= 3.0" 39 | }, 40 | "devDependencies": { 41 | "typescript": "4.7.4", 42 | "release-it": "*" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/dynamic-resolver/event-params.interface.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLExecutionContext } from "@nestjs/graphql" 2 | import type { GraphQLResolveInfo } from "graphql" 3 | 4 | export interface IOnResolvingParams

{ 5 | /** 6 | * The parent object. 7 | * 8 | * @type {P} 9 | * @memberof IOnResolvingParams 10 | */ 11 | readonly parent: P 12 | 13 | /** 14 | * The graphql resolve info. 15 | * 16 | * @type {GraphQLResolveInfo} 17 | * @memberof IOnResolvingParams 18 | */ 19 | readonly resolveInfo: GraphQLResolveInfo 20 | 21 | /** 22 | * The execution context. 23 | * 24 | * @type {GraphQLExecutionContext} 25 | * @memberof IOnResolvingParams 26 | */ 27 | readonly context: GraphQLExecutionContext 28 | 29 | /** 30 | * The root object. 31 | * 32 | * @type {TRoot} 33 | * @memberof IOnResolvingParams 34 | */ 35 | readonly root: TRoot 36 | } 37 | 38 | export interface IOnResolvedParams extends IOnResolvingParams { 39 | /** 40 | * The data resolved by the resolver. 41 | * 42 | * @type {R} 43 | * @memberof IOnResolvedParams 44 | */ 45 | data: R 46 | 47 | /** 48 | * Indicates if the {@link data} is resolved from the database or not. 49 | * 50 | * This property will be `false` if the {@link data} is got from the 51 | * {@link IUseDynamicResolversParams.onResolving} method. 52 | * 53 | * @type {boolean} 54 | * @memberof IOnResolvedParams 55 | */ 56 | readonly fromDatabase: boolean 57 | } 58 | -------------------------------------------------------------------------------- /src/helpers/pluralize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks whether the input is ending with any of the given endings. 3 | * 4 | * @param {string} input The input string. 5 | * @param {...string[]} endings The possible endings. 6 | * @return {boolean} `true` if the string was ending with one of the passed 7 | * endings. 8 | */ 9 | function endsWithAny(input: string, ...endings: string[]) { 10 | return endings.some(ending => input.endsWith(ending)) 11 | } 12 | 13 | /** 14 | * Finds the last vowel in the input. 15 | * 16 | * The search starts from the end of the string. 17 | * 18 | * @param {string} input The input string. 19 | * @return {([ string | undefined, number ])} A tuple of the last vowel 20 | * string and its index. The index will be `-1` if not found and the string 21 | * will be `undefined`. 22 | */ 23 | function findLastVowel(input: string): [ string | undefined, number ] { 24 | for (let i = input.length - 1; i >= 0; --i) { 25 | const letter = input[ i ] 26 | if (isVowel(letter)) return [ letter, i ] 27 | } 28 | 29 | return [ , -1 ] 30 | } 31 | 32 | /** 33 | * Checks whether the given character is a vowel or not. 34 | * 35 | * @param {string} char The character input. 36 | * @return {boolean} `true` if the given input was a vowel. 37 | */ 38 | function isVowel(char: string) { 39 | return 'aeiou'.indexOf(char) >= 0 40 | } 41 | 42 | /** 43 | * Pluralizes an input with basic English language rules. 44 | * 45 | * @param {string} input - The input string to pluralize. 46 | * @return {string} A function that takes a string and returns a pluralized 47 | * version of that string. 48 | */ 49 | export function pluralizeBasic(input: string) { 50 | if (endsWithAny(input, 's', 'x', 'sh', 'ch', 'ss', 'z')) { 51 | return `${input}es` 52 | } 53 | else if (endsWithAny(input, 'y')) { 54 | const [ _, index ] = findLastVowel(input) 55 | if (index < 0) return `${index}s` 56 | 57 | if (index == input.length - 2) { 58 | return `${index}s` 59 | } 60 | else { 61 | return `${input.slice(0, -1)}ies` 62 | } 63 | } 64 | else if (endsWithAny(input, 'ief', 'oof', 'eef', 'ff', 'rf')) { 65 | return `${input}s` 66 | } 67 | else if (endsWithAny(input, 'fe')) { 68 | return `${input.slice(0, -2)}ves` 69 | } 70 | else if (endsWithAny(input, 'f')) { 71 | return `${input.slice(0, -1)}ves` 72 | } 73 | else if (endsWithAny(input, 'o')) { 74 | if (isVowel(input[ input.length - 2 ])) return `${input}s` 75 | return `${input}es` 76 | } 77 | 78 | return `${input}s` 79 | } 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | This file contains the changes made to the package. 3 | 4 | The sections are in descending order of the change date. 5 | 6 | ## [0.1.17] - 2023-07-17 7 | ### Changed 8 | - `dependencies` are moved to `peerDependencies`. 9 | 10 | ## [0.1.16] - 2022-08-21 11 | ### Added 12 | - Basic pluralization for reverse table names if one of the links in navigation 13 | is an array. This will be default and used if not set by the developer. 14 | 15 | ### Changed 16 | - The structure of the folders. 17 | - `mappers` for custom names for cross table references are not used anymore and 18 | planned to be removed in the following versions. 19 | 20 | ## [0.1.15] - 2022-08-20 21 | ### Fixed 22 | - `Array.map` error during `*:1` relation mapping. 23 | 24 | ## [0.1.14] - 2022-08-16 25 | ### Fixed 26 | - Prisma Selections for deeply nested objects were not working correctly. 27 | 28 | ## Added 29 | - Better way to handle renaming. 30 | 31 | ## [0.1.9] - 2022-04-27 32 | ### Changed 33 | - `IOnResolvingParams.context` property. 34 | - `IOnResolvingParams.root` property. 35 | - `IOnResolvedParams.fromDatabase` property. 36 | - Now if a data is returned from `IUseDynamicResolversParams.onResolving` then 37 | the returned data will be available in `IUseDynamicResolversParams.onResolved` 38 | function. Previously, the `onResolved` function was not called if returned 39 | from `onResolving`. 40 | 41 | ## [0.1.7] - 2022-04-27 42 | ### Changed 43 | - Type improvements for `selectionMap` option. 44 | 45 | ## [0.1.6] - 2022-04-27 46 | ### Added 47 | - Changelog file. 48 | - `keepNavigationMap` option to `@UseDynamicResolver()` decorator parameter 49 | object. 50 | 51 | ### Changed 52 | - `getNavigationMap` function name to `getNavigationMaps`. 53 | 54 | ## [0.1.5] - 2022-04-26 55 | ### Added 56 | The initial version of the package. 57 | 58 | [Unreleased]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/v1.0.0...HEAD 59 | [0.1.16]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/0.1.15...0.1.16 60 | [0.1.15]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/0.1.14...0.1.15 61 | [0.1.14]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/0.1.13...0.1.14 62 | [0.1.13]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/0.1.9...0.1.13 63 | [0.1.9]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/0.1.7...0.1.9 64 | [0.1.7]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/0.1.6...0.1.7 65 | [0.1.6]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/compare/0.1.5...0.1.6 66 | [0.1.5]: https://github.com/incetarik/nestjs-prisma-dynamic-resolvers/releases/tag/0.1.5 67 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The function interface. 3 | */ 4 | export type Func = (...args: A) => R 5 | 6 | /** 7 | * Gets the keys of type `T` matching the `V` values. 8 | */ 9 | export type KeysMatching = { [ K in keyof T ]-?: T[ K ] extends V ? K : never }[ keyof T ] 10 | 11 | /** 12 | * A Dictionary type for keeping `TValue` values. 13 | */ 14 | export type Dictionary = { [ key: string | symbol ]: TValue } 15 | 16 | /** 17 | * Maps the given type to its function keys. 18 | * If the given type is an array, then the function indices will be returned. 19 | * If the given type is an object, then the function keys will be returned. 20 | */ 21 | export type FunctionKeys 22 | = T extends any[] 23 | ? Exclude<_FunctionKeys, -1> 24 | : T extends { [ key: string ]: any } 25 | ? KeysMatching 26 | : never 27 | 28 | /** 29 | * Defines the nullable version of given type. 30 | */ 31 | export type Nullable = T | null | undefined 32 | 33 | /** 34 | * Defines the nullable version of given type with `void` type. 35 | */ 36 | export type NullableReturn = Nullable | void 37 | 38 | /** 39 | * Defines a type which can be in a promise or not. 40 | */ 41 | export type MaybePromise = Promise | T 42 | 43 | /** 44 | * Gets the keys of an object whose values are objects. 45 | * 46 | * This does not include function keys. 47 | */ 48 | export type ObjectKeys 49 | = Exclude, FunctionKeys> 50 | 51 | /** 52 | * Removes the nullable keys from an object. 53 | */ 54 | export type RemoveNullables = { 55 | [ P in keyof T as T[ P ] extends K ? never : P ]: T[ P ] extends object 56 | ? RemoveNullables 57 | : T[ P ] 58 | } 59 | 60 | /** 61 | * Gets the length of an array. 62 | */ 63 | export type Length = T extends { length: infer L } ? L : never 64 | 65 | /** 66 | * Builds a tuple with given length. 67 | */ 68 | export type BuildTuple 69 | = T extends { length: L } ? T : BuildTuple 70 | 71 | /** 72 | * Defines a recursive record of `T` values. 73 | */ 74 | export type RecursiveRecord = { 75 | [ key in string ]: T | RecursiveRecord 76 | } 77 | 78 | /** 79 | * Maps a record values to `NewValue`. 80 | */ 81 | export type MapRecordValues 82 | = { 83 | [ K in keyof T ]: T[ K ] extends infer V 84 | ? ( 85 | V extends object ? MapRecordValues : NewValue 86 | ) 87 | : T[ K ] 88 | } 89 | 90 | 91 | type _FunctionKeys 92 | = TArray extends [ infer H, ...infer T ] 93 | ? H extends Func 94 | ? _FunctionKeys, Index | CurrentIndex> 95 | : _FunctionKeys, Index> 96 | : Index 97 | 98 | type Add = 99 | Length<[ ...BuildTuple, ...BuildTuple ]> & number 100 | -------------------------------------------------------------------------------- /src/navigation-property.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common' 2 | 3 | import { INavigationMap, registerNavigation } from './dynamic-navigations' 4 | import { pluralizeBasic, toCamelCase } from './helpers' 5 | 6 | /** 7 | * A Symbol for accessing the navigation map of a type. 8 | */ 9 | export const SYM_MAP = Symbol('[[NavigationMap]]') 10 | 11 | interface INavigationPropertyParameters { 12 | /** 13 | * The target class. 14 | * 15 | * @type {Type} 16 | * @memberof IParams 17 | */ 18 | target: Type 19 | 20 | /** 21 | * The name of the target class table. 22 | * 23 | * @type {string} 24 | * @memberof IParams 25 | */ 26 | tableName?: string 27 | 28 | /** 29 | * The table name of the source class from the target class. 30 | * This property will be used for inverse selections. 31 | * 32 | * @type {string} 33 | * @memberof IParams 34 | */ 35 | reverseNavigationName?: string 36 | } 37 | 38 | /** 39 | * Decorates a table property that is used for accessing another table object. 40 | * The navigation between the tables will be created automatically. 41 | * 42 | * @export 43 | * @param {Type} target The target class. 44 | * @param {string} [tableName] The name of the target table class in the 45 | * Prisma datatable objects. 46 | * 47 | * @return {PropertyDecorator} A {@link PropertyDecorator}. 48 | */ 49 | export function NavigationProperty( 50 | target: Type, 51 | tableName?: string 52 | ): PropertyDecorator 53 | 54 | 55 | /** 56 | * Decorates a table property that is used for accessing another table object. 57 | * The navigation between the tables will be created automatically. 58 | * 59 | * @export 60 | * @param {INavigationPropertyParameters} params The parameters for navigation. 61 | * @return {PropertyDecorator} A {@link PropertyDecorator}. 62 | */ 63 | export function NavigationProperty( 64 | params: INavigationPropertyParameters 65 | ): PropertyDecorator 66 | 67 | 68 | export function NavigationProperty( 69 | paramsOrTarget: INavigationPropertyParameters | Type, 70 | tableName?: string 71 | ): PropertyDecorator { 72 | 73 | let params: INavigationPropertyParameters 74 | if (typeof paramsOrTarget === 'function') { 75 | params = { 76 | target: paramsOrTarget, 77 | tableName 78 | } 79 | } 80 | else { 81 | params = paramsOrTarget 82 | } 83 | 84 | return function _NavigationProperty(source: Object, propertyName: string | symbol) { 85 | const type = Reflect.getMetadata('design:type', source, propertyName) 86 | const isArray = type === Array 87 | 88 | const { 89 | target, 90 | tableName = source.constructor.name, 91 | reverseNavigationName = isArray ? pluralizeBasic(tableName) : tableName 92 | } = params 93 | 94 | const sourceTableName = toCamelCase(tableName) 95 | const targetTableName = toCamelCase(target.name) 96 | 97 | const navigationMaps = _getOrCreateNavigationMap(source.constructor) 98 | const navigationMap = navigationMaps[ propertyName as string ] = { 99 | source: source.constructor, 100 | target, 101 | sourceTableName, 102 | targetTableName, 103 | sourceProperty: propertyName as string, 104 | reverseTableName: reverseNavigationName, 105 | relation: isArray ? '1:*' : '1:1', 106 | } 107 | 108 | registerNavigation({ 109 | from: { 110 | source: navigationMap.source as Type, 111 | withProperty: navigationMap.sourceProperty, 112 | tableName: navigationMap.sourceTableName, 113 | reverseTableName: navigationMap.reverseTableName, 114 | }, 115 | to: { 116 | target: navigationMap.target, 117 | tableName: navigationMap.targetTableName, 118 | }, 119 | relation: navigationMap.relation, 120 | }) 121 | } 122 | } 123 | 124 | function _getOrCreateNavigationMap(target: Object) { 125 | let maps: Record 126 | = Reflect.getMetadata(SYM_MAP, target) 127 | 128 | if (maps) return maps 129 | Reflect.defineMetadata(SYM_MAP, maps = {}, target) 130 | return maps 131 | } 132 | -------------------------------------------------------------------------------- /src/dynamic-resolver/dynamic-resolver-ops.ts: -------------------------------------------------------------------------------- 1 | import type { Type } from "@nestjs/common" 2 | import type { IOnResolvedParams, IOnResolvingParams } from "./event-params.interface" 3 | 4 | /** 5 | * An interface containing parameters for {@link UseDynamicResolvers} decorator. 6 | * 7 | * @export 8 | * @interface IUseDynamicResolversParams 9 | */ 10 | export interface IUseDynamicResolversParams { 11 | /** 12 | * The name of the module that will contain the resolver. 13 | * 14 | * This name is used for grouping the resolvers and then the grouped resolvers 15 | * can be acquired through {@link NavigationProperty.getDynamicResolvers}. 16 | * 17 | * If the name is not given, then they will be registered as global. 18 | * 19 | * @type {string} 20 | * @memberof IUseDynamicResolversParams 21 | */ 22 | moduleName?: string 23 | 24 | /** 25 | * The name of the database table. 26 | * 27 | * This will be used for generating `select` / `include` clauses. 28 | * 29 | * @type {string} 30 | * @memberof IUseDynamicResolversParams 31 | */ 32 | tableName?: string 33 | 34 | /** 35 | * The name of the property that is used as ID property of the database table. 36 | * 37 | * @type {string} 38 | * @memberof IUseDynamicResolversParams 39 | * @default 'id 40 | */ 41 | primaryKeyName?: string 42 | 43 | /** 44 | * Indicates if the generated navigation map should be kept as metadata of the 45 | * target type. 46 | * 47 | * Set this property to `true` to keep the navigation map and use 48 | * {@link getNavigationMapsOf} function to get the navigation map of the type. 49 | * 50 | * @type {boolean} 51 | * @memberof IUseDynamicResolversParams 52 | * @default false 53 | */ 54 | keepNavigationMap?: boolean 55 | 56 | /** 57 | * An event function that will be triggered when the resolver is about to resolve the defined navigation. 58 | * 59 | * @param {IOnResolvingParams

} params The parameters. 60 | * @return {*} The 61 | * replace value. 62 | * 63 | * If this function returns a value from this event function, then the 64 | * resolving will be cancelled and the data returned from this function will 65 | * be used as the result of resolving. 66 | * 67 | * @memberof IUseDynamicResolversParams 68 | */ 69 | onResolving?

(params: IOnResolvingParams): any 70 | 71 | /** 72 | * An event function that will be triggered when the resolved is resolved the defined navigation. 73 | * 74 | * @param {IOnResolvedParams} params The parameters. 75 | * @return {*} The 76 | * replace value. 77 | * 78 | * If this function returns a value from this event function, then the 79 | * resolving will be ignored and the data returned from this function will be 80 | * used as the result of the resolving. 81 | * 82 | * @memberof IUseDynamicResolversParams 83 | */ 84 | onResolved?

(params: IOnResolvedParams): any 85 | } 86 | 87 | export interface IRegisterDynamicResolverParams extends IUseDynamicResolversParams { 88 | /** 89 | * The target class that uses the dynamic resolvers. 90 | * 91 | * @type {Type} 92 | * @memberof IUseDynamicResolversParams 93 | */ 94 | target: Type 95 | } 96 | 97 | let _resolverParams: IRegisterDynamicResolverParams[] | undefined 98 | 99 | /** 100 | * Registers a type as dynamic resolver. 101 | * 102 | * @export 103 | * @param {IUseDynamicResolversParams} params The parameters. 104 | */ 105 | export function registerDynamicResolver(params: IRegisterDynamicResolverParams) { 106 | _resolverParams ??= [] 107 | _resolverParams.push(params) 108 | } 109 | 110 | /** 111 | * Gets the resolvers that are created dynamically. 112 | * 113 | * @export 114 | * @param {string} [groupName='_global'] The name of the group. 115 | * @return {Type[]} An array of resolver classes of the group. 116 | */ 117 | export function getDynamicResolverParams(groupName = '_global') { 118 | if (!_resolverParams) return [] 119 | return _resolverParams.filter(it => it.moduleName === groupName) 120 | } 121 | 122 | /** 123 | * Removes the dynamic resolver parameters of a group. 124 | * 125 | * @export 126 | * @param {string} groupName The name of the groups. 127 | * @return {boolean} A boolean indicating the operation state. If `false`, then 128 | * nothing is done. 129 | */ 130 | export function removeDynamicResolverParamsOfGroup(groupName: string) { 131 | if (!_resolverParams) return false 132 | 133 | for (let i = _resolverParams.length - 1; i >= 0; --i) { 134 | const item = _resolverParams[ i ] 135 | if (item.moduleName === groupName) { 136 | _resolverParams.splice(i, 1) 137 | } 138 | } 139 | 140 | if (_resolverParams.length === 0) { 141 | _resolverParams = undefined 142 | } 143 | 144 | return true 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nestjs-prisma-dynamic-resolvers 2 | 3 | This package allows you to dynamically resolve the relations between Prisma 4 | models in your Nestjs application. 5 | 6 | The usage is pretty simple and the defining navigations between two different 7 | Prisma models are possible in two different ways: 8 | - Using the `@NavigationProperty` decorator. 9 | - Using the `registerNavigation` function. 10 | 11 | After defining your navigations, the application will not have its effect 12 | immediately to prevent unexpected scenarios during the setup process of 13 | your module. This also lets the developer to comment out a single line to debug 14 | the navigation effect between models. 15 | 16 | Use the `@UseDynamicResolvers` decoraor on your model classes which have 17 | navigation to another model classes and with these decorators/functions the 18 | navigation setup will be completed. 19 | 20 | As this package is designed to be used with the Prisma package, you should 21 | provide your `PrismaClient` instance to the dynamic resolvers. 22 | 23 | ## Setup Example 24 | 25 | Assume you have the following model classes: 26 | 27 | ```ts 28 | // user.ts 29 | 30 | import { UserRole } from './user-role' 31 | import { ID, Field } from '@nestjs/graphql' 32 | import { NavigationProperty, UseDynamicResolvers } from 'nestjs-prisma-dynamic-resolvers' 33 | 34 | @ObjectType() 35 | @UseDynamicResolvers({ moduleName: 'user' }) 36 | export class User { 37 | @Field(() => ID) 38 | id!: string 39 | 40 | @Field() 41 | name!: string 42 | 43 | @Field() 44 | surname!: string 45 | 46 | @Field(() => [ UserRole ], { 47 | defaultValue: [] 48 | }) 49 | @NavigationProperty({ target: UserRole }) 50 | roles: UserRole[] = [] 51 | } 52 | 53 | ``` 54 | 55 | ```ts 56 | // user-role.ts 57 | 58 | import { ID, Field } from '@nestjs/graphql' 59 | import { UseDynamicResolvers } from 'nestjs-prisma-dynamic-resolvers' 60 | 61 | @ObjectType() 62 | @UseDynamicResolvers({ moduleName: 'user' }) 63 | export class UserRole { 64 | @Field(() => ID) 65 | id!: string 66 | 67 | @Field() 68 | name!: string 69 | } 70 | 71 | ``` 72 | 73 | ```ts 74 | // user.module.ts 75 | import { provideDynamicResolvers } from 'nestjs-prisma-dynamic-resolvers' 76 | 77 | @Module({ 78 | // ... 79 | providers: [ 80 | // ... 81 | PrismaService, 82 | ...provideDynamicResolvers(PrismaService, 'user') 83 | ] 84 | // ... 85 | }) 86 | export class UserModule {} 87 | ``` 88 | 89 | And that's it! Now you can execute the following `GraphQL` query easily: 90 | 91 | _-- Assuming you have `allUsers` query_ 92 | 93 | ```gql 94 | query { 95 | allUsers { 96 | id 97 | name 98 | surname 99 | 100 | roles { 101 | id 102 | name 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | ## Advanced Example 109 | 110 | Now the best part of this library is when you have nested references between 111 | database table models. Like, when a class is referencing another class that 112 | also references another class which all are also Prisma models. 113 | 114 | In addition to that, as JavaScript/TypeScript is not allowing recursive imports 115 | we will define navigations in another file. 116 | 117 | Assuming you have another class: 118 | ```ts 119 | // user-claim.ts 120 | import { ID, Field } from '@nestjs/graphql' 121 | import { UserRole } from './user-role' 122 | import { NavigationProperty, UseDynamicResolvers } from 'nestjs-prisma-dynamic-resolvers' 123 | 124 | @ObjectType() 125 | @UseDynamicResolvers({ moduleName: 'user' }) 126 | export class UserClaim { 127 | @Field(() => ID) 128 | id!: string 129 | 130 | @Field() 131 | name!: string 132 | 133 | @Field(() => [ UserRole ], { 134 | defaultValue: [] 135 | }) 136 | roles: UserRole[] = [] 137 | } 138 | ``` 139 | 140 | And your `User` class is updated to this: 141 | ```ts 142 | // user.ts 143 | import { UserClaim } from './user-claim' 144 | import { UserRole } from './user-role' 145 | import { ID, Field } from '@nestjs/graphql' 146 | import { NavigationProperty, UseDynamicResolvers } from 'nestjs-prisma-dynamic-resolvers' 147 | 148 | @ObjectType() 149 | @UseDynamicResolvers({ moduleName: 'user' }) 150 | export class User { 151 | @Field(() => ID) 152 | id!: string 153 | 154 | @Field() 155 | name!: string 156 | 157 | @Field() 158 | surname!: string 159 | 160 | @Field(() => [ UserRole ], { 161 | defaultValue: [] 162 | }) 163 | @NavigationProperty({ target: UserRole }) 164 | roles: UserRole[] = [] 165 | 166 | @Field(() => [ UserRole ], { 167 | defaultValue: [] 168 | }) 169 | @NavigationProperty({ target: UserClaim }) 170 | claims: UserClaim[] = [] 171 | } 172 | 173 | ``` 174 | 175 | And assume your `UserRole` class also have `UserClaim`s: 176 | ```ts 177 | // user-role.ts 178 | 179 | import { UserClaim } from './user-claim' 180 | import { ID, Field } from '@nestjs/graphql' 181 | import { NavigationProperty, UseDynamicResolvers } from 'nestjs-prisma-dynamic-resolvers' 182 | 183 | @ObjectType() 184 | @UseDynamicResolvers({ moduleName: 'user' }) 185 | export class UserRole { 186 | @Field(() => ID) 187 | id!: string 188 | 189 | @Field() 190 | name!: string 191 | 192 | @Field(() => [ UserClaim ], { 193 | defaultValue: [] 194 | }) 195 | claims: UserClaim[] = [] 196 | } 197 | 198 | ``` 199 | 200 | In another file importing these two circular classes: 201 | ```ts 202 | // index.ts 203 | import { UserClaim } from './user-claim' 204 | import { UserRole } from './user-role' 205 | import { registerNavigation } from 'nestjs-prisma-dynamic-resolvers' 206 | 207 | registerNavigation({ 208 | from: { 209 | source: UserRole, 210 | withProperty: 'claims', 211 | }, 212 | to: { 213 | target: UserClaim, 214 | withProperty: 'roles', 215 | }, 216 | relation: '*:*', 217 | }) 218 | ``` 219 | 220 | Now even with these recursively dependent, nested classes, the dynamic 221 | resolvers will still be generated from one model to another and the complex 222 | queries will still work. 223 | 224 | Example `GraphQL` Query: 225 | 226 | ```gql 227 | query { 228 | allUsers { 229 | id 230 | name 231 | surname 232 | 233 | claims { 234 | name 235 | } 236 | 237 | roles { 238 | id 239 | name 240 | 241 | claims { 242 | id 243 | name 244 | } 245 | } 246 | } 247 | } 248 | ``` 249 | 250 | # Support 251 | To support the project, you can send donations to following addresses: 252 | 253 | ```md 254 | - Bitcoin : bc1qtut2ss8udkr68p6k6axd0na6nhvngm5dqlyhtn 255 | - Bitcoin Cash: qzmmv43ztae0tfsjx8zf4wwnq3uk6k7zzgcfr9jruk 256 | - Ether : 0xf542BED91d0218D9c195286e660da2275EF8eC84 257 | ``` 258 | -------------------------------------------------------------------------------- /src/dynamic-resolver/make-dynamic-resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Type } from '@nestjs/common' 2 | import { 3 | Context, 4 | GraphQLExecutionContext, 5 | Info, 6 | Parent, 7 | ResolveField, 8 | Resolver, 9 | Root, 10 | } from '@nestjs/graphql' 11 | 12 | import { SYM_PRISMA_CLIENT } from '../constants' 13 | import { 14 | getNavigationMapsOf, 15 | INavigationMap, 16 | removeNavigationMapsOf, 17 | } from '../dynamic-navigations' 18 | import { DynamicResolverBase } from './dynamic-resolver-base' 19 | import { 20 | getDynamicResolverParams, 21 | IRegisterDynamicResolverParams, 22 | removeDynamicResolverParamsOfGroup, 23 | } from './dynamic-resolver-ops' 24 | 25 | import type { GraphQLResolveInfo } from "graphql" 26 | import type { Dictionary } from "../types" 27 | 28 | interface IMakeDynamicResolverParams extends IRegisterDynamicResolverParams { 29 | selectionMap?: Dictionary 30 | navigationMap: INavigationMap 31 | } 32 | 33 | function _makeDynamicResolver(params: IMakeDynamicResolverParams) { 34 | const { 35 | target, 36 | navigationMap, 37 | primaryKeyName = 'id', 38 | 39 | onResolving, 40 | onResolved, 41 | } = params 42 | 43 | @Resolver(() => target, { isAbstract: true }) 44 | class DynamicResolver extends DynamicResolverBase { 45 | constructor(@Inject(SYM_PRISMA_CLIENT) protected readonly prisma: any) { 46 | super(prisma) 47 | } 48 | 49 | protected async fireOnResolvingEvent( 50 | parent: { id: string }, 51 | root: any, 52 | info: GraphQLResolveInfo, 53 | context: GraphQLExecutionContext 54 | ) { 55 | if (typeof onResolving === 'function') { 56 | const replaceValue = await onResolving({ 57 | root, 58 | parent, 59 | context, 60 | resolveInfo: info, 61 | }) 62 | 63 | if (typeof replaceValue === 'object') { 64 | return [ true, replaceValue ] 65 | } 66 | } 67 | 68 | return [ false ] 69 | } 70 | 71 | protected async fireOnResolvedEvent( 72 | parent: { id: string }, 73 | root: any, 74 | info: GraphQLResolveInfo, 75 | data: any, 76 | context: GraphQLExecutionContext, 77 | fromDatabase: boolean 78 | ) { 79 | if (typeof onResolved === 'function') { 80 | const replaceValue = await onResolved({ 81 | data, 82 | root, 83 | parent, 84 | context, 85 | fromDatabase, 86 | resolveInfo: info, 87 | }) 88 | 89 | if (typeof replaceValue === 'object') { 90 | return replaceValue 91 | } 92 | } 93 | 94 | return data 95 | } 96 | } 97 | 98 | const { 99 | source, 100 | relation, 101 | sourceProperty, 102 | } = navigationMap 103 | 104 | const isArray = relation.indexOf('*') >= 0 105 | 106 | const handlerMethodDescriptor: PropertyDescriptor = { 107 | async value( 108 | this: DynamicResolver, 109 | parent: { id: string }, 110 | root: any, 111 | info: GraphQLResolveInfo, 112 | context: GraphQLExecutionContext 113 | ) { 114 | const [ 115 | shouldReturn, 116 | replaceValue 117 | ] = await this.fireOnResolvingEvent(parent, root, info, context) 118 | 119 | if (shouldReturn) { 120 | return this.fireOnResolvedEvent( 121 | parent, 122 | root, 123 | info, 124 | replaceValue, 125 | context, 126 | false 127 | ) 128 | } 129 | 130 | const data = await this.resolve( 131 | parent, 132 | primaryKeyName, 133 | info, 134 | navigationMap, 135 | params.selectionMap 136 | ) 137 | 138 | return this.fireOnResolvedEvent(parent, root, info, data, context, true) 139 | } 140 | } 141 | 142 | Object.defineProperty( 143 | DynamicResolver.prototype, 144 | sourceProperty, 145 | handlerMethodDescriptor 146 | ) 147 | 148 | Parent()(DynamicResolver.prototype, sourceProperty, 0) 149 | Root()(DynamicResolver.prototype, sourceProperty, 1) 150 | Info()(DynamicResolver.prototype, sourceProperty, 2) 151 | Context()(DynamicResolver.prototype, sourceProperty, 3) 152 | 153 | const resolvedType = isArray ? [ target ] : target 154 | 155 | ResolveField(() => resolvedType, { 156 | description: 157 | `Resolves the ${sourceProperty} of the ${source.constructor.name}.` 158 | })(DynamicResolver.prototype, sourceProperty, handlerMethodDescriptor) 159 | 160 | return DynamicResolver 161 | } 162 | 163 | /** 164 | * Generates the registered dynamic resolvers and returns the resolver 165 | * classes. 166 | * 167 | * @export 168 | * @param {string} [groupName='_global'] The group name of the resolvers 169 | * that will be generated. 170 | * 171 | * @param {boolean} [freeMemory=true] Indicates if the memory should be freed 172 | * after the generation. If this is set to `false`, then 173 | * {@link getDynamicResolverParams} will still return the resolver parameters. 174 | * 175 | * @return {Type[]} An array of the resolver classes. 176 | */ 177 | export function generateDynamicResolvers( 178 | groupName = '_global', 179 | freeMemory = true 180 | ) { 181 | const resolverParams = getDynamicResolverParams(groupName) 182 | const resolverClasses: Dictionary = {} 183 | 184 | for (const params of resolverParams) { 185 | const { target, keepNavigationMap = false } = params 186 | const [ navigationMaps, selectionMap ] = _generateMapsForType(target) 187 | if (!navigationMaps) continue 188 | 189 | const resolversOfTarget: Type[] = [] 190 | 191 | for (const navigationMap of navigationMaps) { 192 | const dynamicResolver = _makeDynamicResolver({ 193 | ...params, 194 | navigationMap, 195 | selectionMap 196 | }) 197 | 198 | resolversOfTarget.push(dynamicResolver) 199 | } 200 | 201 | resolverClasses[ target.name ] = resolversOfTarget 202 | 203 | if (!keepNavigationMap) { 204 | removeNavigationMapsOf(target) 205 | } 206 | } 207 | 208 | if (freeMemory) { 209 | removeDynamicResolverParamsOfGroup(groupName) 210 | } 211 | 212 | return resolverClasses 213 | } 214 | 215 | function _generateMapsForType(sourceClass: Type): [ 216 | navigatioMaps?: INavigationMap[], 217 | selectionMap?: Dictionary 218 | ] { 219 | const navigationMaps = getNavigationMapsOf(sourceClass) 220 | if (!navigationMaps.length) return [] 221 | 222 | const selectionMap = navigationMaps.reduce((prev, current) => { 223 | const { 224 | sourceProperty, 225 | sourceTableName, 226 | targetProperty, 227 | targetTableName 228 | } = current 229 | 230 | prev[ sourceProperty ] = sourceTableName 231 | 232 | if (targetProperty) { 233 | prev[ targetProperty ] = targetTableName 234 | } 235 | 236 | return prev 237 | }, { 238 | sourceClass: sourceClass as any 239 | } as Dictionary) 240 | 241 | return [ navigationMaps, selectionMap ] 242 | } 243 | -------------------------------------------------------------------------------- /src/dynamic-navigations.ts: -------------------------------------------------------------------------------- 1 | import type { Type } from '@nestjs/common' 2 | 3 | import { toCamelCase } from './helpers' 4 | 5 | import type { KeysMatching } from './types' 6 | 7 | let _navigations: INavigationMap[] | undefined 8 | 9 | /** 10 | * The relation string. 11 | */ 12 | type RelationString = '1:1' | '1:*' | '*:1' | '*:*' 13 | 14 | /** 15 | * Contains the information about a navigation link from a table object to 16 | * another. 17 | * 18 | * @export 19 | * @interface INavigationMap 20 | */ 21 | export interface INavigationMap { 22 | /** 23 | * The prototype of the source class. 24 | * 25 | * @type {Function} 26 | * @memberof INavigationMap 27 | */ 28 | source: Function 29 | 30 | /** 31 | * The target type constructor. 32 | * 33 | * @type {Type} 34 | * @memberof INavigationMap 35 | */ 36 | target: Type 37 | 38 | /** 39 | * The table name of the source class. 40 | * 41 | * @type {string} 42 | * @memberof INavigationMap 43 | */ 44 | sourceTableName: string 45 | 46 | /** 47 | * The table name of the source class, used for navigating back from the 48 | * target class. 49 | * 50 | * This property is useful for finding all rows if the property is an array. 51 | * By default, this will be equal to {@link sourceTableName} if not passed. 52 | * 53 | * @type {string} 54 | * @memberof INavigationMap 55 | * @default `${sourceTableName}` 56 | */ 57 | reverseTableName?: string 58 | 59 | /** 60 | * The property name of the source class. 61 | * 62 | * @type {string} 63 | * @memberof INavigationMap 64 | */ 65 | sourceProperty: string 66 | 67 | /** 68 | * The table name of the target class. 69 | * 70 | * @type {string} 71 | * @memberof INavigationMap 72 | */ 73 | targetTableName: string 74 | 75 | /** 76 | * The property name of the target class. 77 | * 78 | * Required if {@link relation} is `*:*`. 79 | * 80 | * @type {string} 81 | * @memberof INavigationMap 82 | */ 83 | targetProperty?: string 84 | 85 | /** 86 | * The relation between two classes. 87 | * 88 | * @type {RelationString} 89 | * @memberof INavigationMap 90 | */ 91 | relation: RelationString 92 | } 93 | 94 | /** 95 | * Defines a type of information of a type of a database table model that 96 | * is used for navigation. 97 | * 98 | * @interface INavigationTableInfo 99 | * @template A The type of the first database table model. 100 | * @template B The type of the second database table model. 101 | */ 102 | interface INavigationTableInfo { 103 | /** 104 | * The name of the model in the database. 105 | * 106 | * @type {string} 107 | * @memberof INavigationTableInfo 108 | */ 109 | tableName?: string 110 | 111 | /** 112 | * The name of the property of the database table in the model. 113 | * 114 | * @type {(string & ( 115 | * KeysMatching, InstanceType | InstanceType[]> 116 | * ))} 117 | * @memberof INavigationTableInfo 118 | */ 119 | withProperty: string & ( 120 | KeysMatching, InstanceType | InstanceType[]> 121 | ) 122 | } 123 | 124 | type INavigationSourceTableInfo 125 | = INavigationTableInfo & { 126 | /** 127 | * The source database table model. 128 | * 129 | * @type {A} 130 | */ 131 | source: A 132 | 133 | /** 134 | * The table name of the source class, used for navigating back from the 135 | * target class. 136 | * 137 | * This property is useful for finding all rows if the property is an array. 138 | * By default, this will be equal to {@link sourceTableName} if not passed. 139 | * 140 | * @type {string} 141 | * @default `${sourceTableName}` 142 | */ 143 | reverseTableName?: string 144 | } 145 | 146 | type INavigationTargetTableInfo 147 | = Omit, 'withProperty'> & { 148 | /** 149 | * The target database table model. 150 | * 151 | * @type {B} 152 | */ 153 | target: B 154 | 155 | /** 156 | * The name of the property of the database table in the model. 157 | * 158 | * @type {(string & ( 159 | * KeysMatching, InstanceType | InstanceType[]> 160 | * ))} 161 | * @memberof INavigationTableInfo 162 | */ 163 | withProperty?: string & ( 164 | KeysMatching, InstanceType | InstanceType[]> 165 | ) 166 | } 167 | 168 | /** 169 | * Represents the type of a navigation definition between two tables. 170 | * 171 | * @export 172 | * @interface INavigationDefinitonParams 173 | * @template A The type of the first database table model. 174 | * @template B The type of the second database table model. 175 | */ 176 | export interface INavigationDefinitonParams { 177 | /** 178 | * The source information of the database navigation. 179 | * 180 | * @type {INavigationSourceTableInfo} 181 | * @memberof INavigationDefinitonParams 182 | */ 183 | from: INavigationSourceTableInfo 184 | 185 | /** 186 | * The target information of the database navigation. 187 | * 188 | * @type {INavigationTargetTableInfo} 189 | * @memberof INavigationDefinitonParams 190 | */ 191 | to: INavigationTargetTableInfo 192 | 193 | /** 194 | * The relationship between two tables. 195 | * 196 | * @type {RelationString} 197 | * @memberof INavigationDefinitonParams 198 | */ 199 | relation: RelationString 200 | } 201 | 202 | /** 203 | * Registers a database table navigation between models. 204 | * 205 | * @export 206 | * @template A The type of the first/source database table model. 207 | * @template B The type of the second/target database table model. 208 | * @param {INavigationDefinitonParams} params The navigation definition 209 | * parameters. 210 | * 211 | */ 212 | export function registerNavigation(params: INavigationDefinitonParams) { 213 | _navigations ??= [] 214 | const { from, to, relation } = params 215 | 216 | const sourceTableName = toCamelCase(from.tableName ?? from.source.name) 217 | const targetTableName = toCamelCase(to.tableName ?? to.target.name) 218 | 219 | const navigationMap: INavigationMap = { 220 | relation, 221 | sourceTableName, 222 | targetTableName, 223 | target: to.target, 224 | source: from.source, 225 | targetProperty: to.withProperty, 226 | sourceProperty: from.withProperty!, 227 | } 228 | 229 | if (relation === '*:*') { 230 | const left: INavigationMap = { 231 | ...navigationMap, 232 | relation: '1:*' 233 | } 234 | 235 | if (!left.targetProperty) { 236 | throw new Error(`[registerNavigation targetProperty] - Property is required when the relation is '*:*'`) 237 | } 238 | 239 | const right: INavigationMap = { 240 | source: left.target, 241 | target: left.source as Type, 242 | sourceProperty: left.targetProperty, 243 | targetProperty: left.sourceProperty, 244 | sourceTableName: left.targetTableName, 245 | targetTableName: left.sourceTableName, 246 | relation: '1:*' 247 | } 248 | 249 | _navigations.push(left, right) 250 | } 251 | else { 252 | _navigations.push({ 253 | relation, 254 | sourceTableName, 255 | targetTableName, 256 | target: to.target, 257 | source: from.source, 258 | targetProperty: to.withProperty, 259 | sourceProperty: from.withProperty, 260 | }) 261 | } 262 | } 263 | 264 | /** 265 | * Gets the all navigation maps registered through {@link registerNavigation}. 266 | * 267 | * @export 268 | * @return {INavigationMap[]} An array of navigation map definitions. 269 | */ 270 | export function getNavigationMaps() { 271 | return _navigations ? [ ..._navigations ] : [] 272 | } 273 | 274 | /** 275 | * Gets the navigation maps defined **from** the given database table model 276 | * type. 277 | * 278 | * @export 279 | * @template A The type of the source database table model. 280 | * @param {A} type The source database table model. 281 | * @return {INavigationMap[]} An array of the navigation map definitions. 282 | */ 283 | export function getNavigationMapsOf(type: A) { 284 | return _navigations?.filter(it => it.source === type) ?? [] 285 | } 286 | 287 | /** 288 | * Removes the registered navigation maps of given database table model. 289 | * 290 | * @export 291 | * @template A The type of the source database table model. 292 | * @param {A} type The source database table model. 293 | * @return {boolean} A boolean indicating the operation state. If `false`, then 294 | * nothing is done. 295 | */ 296 | export function removeNavigationMapsOf(type: A) { 297 | if (!_navigations) return false 298 | 299 | for (let i = _navigations.length - 1; i >= 0; --i) { 300 | const item = _navigations[ i ] 301 | if (item.source === type) { 302 | _navigations.splice(i, 1) 303 | } 304 | } 305 | 306 | if (_navigations.length === 0) { 307 | _navigations = undefined 308 | } 309 | 310 | return true 311 | } 312 | -------------------------------------------------------------------------------- /src/helpers/extract-grapql-selections.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldNode, 3 | GraphQLObjectType, 4 | GraphQLResolveInfo, 5 | GraphQLSchema, 6 | Kind, 7 | SelectionNode, 8 | SelectionSetNode, 9 | } from 'graphql' 10 | 11 | import { getNavigationMaps, INavigationMap } from '../dynamic-navigations' 12 | import { getInnerType } from './get-inner-type' 13 | 14 | import type { ObjectKeys, KeysMatching, RecursiveRecord, MapRecordValues, RemoveNullables } from '../types' 15 | 16 | /** 17 | * The disallowed types that should be skipped during the processing. 18 | */ 19 | type DisallowedTypes = Date | ((...args: any) => any) 20 | 21 | /** 22 | * The disallowed keys whose values are matching with {@link DisallowedTypes}. 23 | */ 24 | type DisallowedKeys = KeysMatching 25 | 26 | /** 27 | * A function that will return the new string key value of a key that is found 28 | * in the parents path. 29 | * 30 | * @param {string[]} parents The parent keys. 31 | * @param {string} keyName The name of the key. 32 | * @return {string} The new key name. 33 | */ 34 | type SelectionNameMapperFn = (parents: string[], keyName: string) => string 35 | 36 | /** 37 | * Selects the keys whose values are primitives (not object, not disallowed). 38 | */ 39 | type PrimitiveKeys 40 | = Exclude | DisallowedKeys> 41 | 42 | /** 43 | * Selects the keys that are allowed. 44 | */ 45 | type AllowedKeys 46 | = PrimitiveKeys 47 | | Exclude 48 | 49 | /** 50 | * The value that selection could be. 51 | */ 52 | type SelectionOutput = string | SelectionNameMapperFn 53 | 54 | /** 55 | * Selects the properties of an object that are allowed. 56 | */ 57 | type SelectObjectProperties< 58 | T extends object, 59 | K extends keyof T = AllowedKeys 60 | > = { 61 | [ k in K ]?: T[ k ] extends infer U 62 | ? (U extends (infer V)[] 63 | ? (V extends DisallowedTypes 64 | ? never 65 | : (V extends object 66 | ? IGraphQLExtractSelectionMap> | SelectionOutput 67 | : SelectionOutput 68 | ) 69 | ) 70 | : (U extends DisallowedTypes 71 | ? never 72 | : (U extends object 73 | ? IGraphQLExtractSelectionMap> | SelectionOutput 74 | : SelectionOutput 75 | ) 76 | ) 77 | ) 78 | : SelectionOutput 79 | } 80 | 81 | /** 82 | * Defines how the fields should be renamed/mapped. 83 | * 84 | * If given keys and values are strings, then whenever the key is matched, 85 | * the value will be placed in the object name. 86 | * If the given value is a function, then the function will contain the 87 | * parent keys as path as string array and the current field name and the 88 | * function should return a string to replace the key. 89 | * 90 | * @export 91 | * @interface IGraphQLExtractSelectionMap 92 | * @template T The type of the source. 93 | * @template K The keys included of the source. 94 | */ 95 | export type IGraphQLExtractSelectionMap< 96 | T extends object = object, 97 | K extends keyof T = AllowedKeys 98 | > = RemoveNullables, void | undefined | null> 99 | 100 | /** 101 | * A recursive type that supports partial `select` mapping of a `Prisma` 102 | * select. 103 | */ 104 | export type IGraphQLPrismaSelect< 105 | T extends object = object, 106 | K extends keyof T = AllowedKeys 107 | > = { 108 | [ k in K ]?: T[ k ] extends DisallowedTypes 109 | ? boolean 110 | : T[ k ] extends any[] 111 | ? boolean 112 | : T[ k ] extends object ? ({ 113 | select: IGraphQLPrismaSelect> 114 | }) | boolean 115 | : boolean 116 | } 117 | 118 | interface IExtractGraphQLSelectionsParams> { 119 | /** 120 | * The root node of the GraphQL request. 121 | * 122 | * @type {FieldNode} 123 | * @memberof IParams 124 | */ 125 | node: FieldNode | SelectionNode 126 | 127 | /** 128 | * The map for renaming the field names to database navigation keys. 129 | * 130 | * @type {IGraphQLExtractSelectionMap} 131 | * @memberof IParams 132 | */ 133 | selectionMap?: IGraphQLExtractSelectionMap 134 | } 135 | 136 | /** 137 | * Extracts the GraphQL field selections into an object that is compatible with 138 | * the `select` property of Prisma queries. 139 | * 140 | * @export 141 | * @param {IExtractGraphQLSelectionsParams} params The parameters for 142 | * extraction. 143 | * 144 | * @return {*} An object of `selected` field names. 145 | * @template T The type of the source. 146 | * @template K The keys included of the source. 147 | */ 148 | export function extractGraphQLSelections< 149 | T extends object = object, 150 | K extends keyof T = AllowedKeys 151 | >(params: IExtractGraphQLSelectionsParams) { 152 | const { node } = params 153 | 154 | const selectionObject 155 | = getGraphQLSelectionsObject(node) as MapRecordValues 156 | 157 | const mappedSelections = modifyGraphQLSelections( 158 | selectionObject, 159 | //@ts-ignore 160 | params.selectionMap 161 | ) 162 | 163 | const values = intoPrismaSelection(mappedSelections, {}) 164 | 165 | let name: string 166 | if (node.kind === Kind.FIELD) { 167 | name = node.name.value 168 | } 169 | else { 170 | name = Object.keys(values)[ 0 ] 171 | } 172 | 173 | const returnValue = values[ name as keyof typeof values ] 174 | if ('select' in returnValue) { 175 | return returnValue[ 'select' ] 176 | } 177 | 178 | return returnValue 179 | } 180 | 181 | /** 182 | * Converts a selected keys object into prisma query selection object. 183 | * 184 | * @template T The type of the object. 185 | * @param {Record} info The selection information object. 186 | * @param {RecursiveRecord} [base={}] The base object for selections. 187 | * @return {IGraphQLPrismaSelect} The prisma selections result. 188 | */ 189 | function intoPrismaSelection(info: Record, base: RecursiveRecord = {}) { 190 | for (const key in info) { 191 | const value = info[ key ] 192 | 193 | if (typeof value === 'object') { 194 | const data = intoPrismaSelection(value, {}) 195 | base[ key ] = { 196 | select: data 197 | } 198 | } 199 | else { 200 | base[ key ] = true 201 | } 202 | } 203 | 204 | return base as IGraphQLPrismaSelect 205 | } 206 | 207 | /** 208 | * Extracts the GraphQL selections into an object. 209 | * 210 | * @export 211 | * @param {SelectionNode} node The root node to start. 212 | * @return {RecursiveRecord} A recursive record of string values. 213 | */ 214 | export function getGraphQLSelectionsObject(node: SelectionNode) { 215 | if (node.kind === Kind.FIELD) { 216 | const { name: { value: name }, selectionSet } = node 217 | const selections = selectionSet?.selections 218 | 219 | if (Array.isArray(selections)) { 220 | const { selections } = selectionSet as SelectionSetNode 221 | const collection: RecursiveRecord = {} 222 | 223 | for (const selection of selections) { 224 | const data = getGraphQLSelectionsObject(selection) 225 | 226 | const previousValue = collection[ name ] 227 | if (typeof previousValue === 'object') { 228 | collection[ name ] = { 229 | ...previousValue, 230 | ...data 231 | } 232 | } 233 | else { 234 | collection[ name ] = data 235 | } 236 | } 237 | 238 | return collection 239 | } 240 | else { 241 | return { [ name ]: name } 242 | } 243 | } 244 | else { 245 | return {} 246 | } 247 | } 248 | 249 | /** 250 | * Applies given selection mappers object to given source. 251 | * 252 | * @export 253 | * @template T The type of the actual object. 254 | * @param {MapRecordValues} source The source object. 255 | * @param {SelectionMapOf} [selectionMap={}] The mapper functions object. 256 | * @param {string[]} [parentKeys=[]] The parent keys that will be passed to 257 | * the functions found in the `selectionMap` parameter. 258 | * 259 | * @return {*} An object whose values are mapped with given mappers object. 260 | */ 261 | export function modifyGraphQLSelections( 262 | source: MapRecordValues, 263 | selectionMap: Partial>> = {}, 264 | parentKeys: string[] = [] 265 | ) { 266 | for (const key in source) { 267 | const value = source[ key ] as string | MapRecordValues 268 | const newParentKeys = [ ...parentKeys, key ] 269 | 270 | if (typeof value === 'object') { 271 | //@ts-ignore 272 | const selMap = selectionMap[ key ] ?? {} 273 | 274 | //@ts-ignore 275 | source[ key ] = modifyGraphQLSelections(value, selMap, newParentKeys) 276 | } 277 | else if ((key as string) in selectionMap) { 278 | //@ts-ignore 279 | const mapper = selectionMap[ key ] 280 | 281 | if (typeof mapper === 'function') { 282 | const newValue = mapper(newParentKeys, value) 283 | //@ts-ignore 284 | source[ key ] = newValue 285 | } 286 | else if (typeof mapper === 'string') { 287 | //@ts-ignore 288 | source[ key ] = mapper 289 | } 290 | } 291 | } 292 | 293 | return source 294 | } 295 | 296 | interface IExtractGraphQLSelectionPathParams, KeysMatching>> { 297 | /** 298 | * The initial path of the request. 299 | * 300 | * @type {GraphQLResolveInfo[ 'path' ]} 301 | * @memberof IExtractGraphQLSelectionPathParams 302 | */ 303 | path?: GraphQLResolveInfo[ 'path' ] 304 | } 305 | 306 | type PathInfo = { 307 | propertyName: string 308 | tableName: string 309 | table?: Function 310 | navigationMaps?: INavigationMap[] 311 | parentTypeName?: string 312 | } 313 | 314 | /** 315 | * Extracts the selection paths of a {@link GraphQLResolveInfo}. 316 | * 317 | * @export 318 | * @param {IExtractGraphQLSelectionPathParams} params The parameters for 319 | * extraction. 320 | * 321 | * @return {string[]} An array containing field names from the outer level to 322 | * the inner level. 323 | * 324 | * @template T The type of the source. 325 | * @template K The keys included of the source. 326 | */ 327 | export function extractGraphQLSelectionPath< 328 | T extends object = object, 329 | K extends keyof T = Exclude, KeysMatching> 330 | >(params: IExtractGraphQLSelectionPathParams): PathInfo[] { 331 | const pathInfo = [] as PathInfo[] 332 | 333 | const { path } = params 334 | if (typeof path !== 'object') return pathInfo 335 | 336 | let current = path as typeof path | undefined 337 | while (current) { 338 | const { key, typename } = current 339 | 340 | if (typeof key === 'string') { 341 | pathInfo.unshift({ 342 | propertyName: key, 343 | tableName: typename!, 344 | }) 345 | } 346 | 347 | current = current.prev 348 | } 349 | 350 | for (let i = 1, limit = pathInfo.length; i < limit; ++i) { 351 | const parentTypeName = pathInfo[ i - 1 ].tableName 352 | pathInfo[ i ].parentTypeName = parentTypeName 353 | } 354 | 355 | return pathInfo 356 | } 357 | 358 | /** 359 | * Updates the table information of a path info from given schema. 360 | * 361 | * @export 362 | * @param {PathInfo} pathInfo The path info that will be updated. 363 | * @param {GraphQLSchema} schema The GraphQL schema to update the path info. 364 | * @return {boolean} `true` if the path info is updated, `false` otherwise. 365 | */ 366 | export function updatePathInfoFromSchema(pathInfo: PathInfo, schema: GraphQLSchema) { 367 | if (pathInfo.table) return false 368 | const { propertyName, tableName } = pathInfo 369 | 370 | if (tableName === 'Query') return false 371 | if (tableName === 'Subscription') return false 372 | if (tableName === 'Mutation') return false 373 | 374 | const schemaType = schema.getType(tableName) 375 | if (!schemaType) return false 376 | 377 | if (typeof (schemaType as GraphQLObjectType)[ 'getFields' ] !== 'function') { 378 | return false 379 | } 380 | 381 | const fields = (schemaType as GraphQLObjectType).getFields() 382 | const selectedField = fields[ propertyName ] 383 | 384 | const selectedType = getInnerType(selectedField.type) 385 | if (!('name' in selectedType)) return false 386 | 387 | const { name } = selectedType 388 | 389 | const maps 390 | = getNavigationMaps().filter(it => it.source.name === name) 391 | 392 | pathInfo.table = maps[ 0 ]?.source 393 | pathInfo.navigationMaps = maps 394 | return true 395 | } 396 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------