Skip to content

Commit 5e137ce

Browse files
authored
Merge pull request #412 from goldcaddy77/411-typed-jsonb-fields
feat(decorators): allow concrete type on JSONField
2 parents f0fbd68 + 3b32b4e commit 5e137ce

File tree

11 files changed

+184
-59
lines changed

11 files changed

+184
-59
lines changed

examples/02-complex-example/generated/binding.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ export interface BaseWhereInput {
138138
deletedById_eq?: String | null
139139
}
140140

141+
export interface EventObjectInput {
142+
params: Array<EventParamInput>
143+
}
144+
145+
export interface EventParamInput {
146+
type: String
147+
name: String
148+
value: JSONObject
149+
}
150+
141151
export interface UserCreateInput {
142152
booleanField?: Boolean | null
143153
dateField?: DateTime | null
@@ -153,7 +163,8 @@ export interface UserCreateInput {
153163
bigIntField?: Float | null
154164
jsonField?: JSONObject | null
155165
jsonFieldNoFilter?: JSONObject | null
156-
stringField: String
166+
typedJsonField?: EventObjectInput | null
167+
stringField?: String | null
157168
noFilterField?: String | null
158169
noSortField?: String | null
159170
noFilterOrSortField?: String | null
@@ -197,6 +208,7 @@ export interface UserUpdateInput {
197208
bigIntField?: Float | null
198209
jsonField?: JSONObject | null
199210
jsonFieldNoFilter?: JSONObject | null
211+
typedJsonField?: EventObjectInput | null
200212
stringField?: String | null
201213
noFilterField?: String | null
202214
noSortField?: String | null
@@ -470,6 +482,16 @@ export interface BaseModelUUID extends BaseGraphQLObject {
470482
version: Int
471483
}
472484

485+
export interface EventObject {
486+
params: Array<EventParam>
487+
}
488+
489+
export interface EventParam {
490+
type: String
491+
name: String
492+
value: JSONObject
493+
}
494+
473495
export interface PageInfo {
474496
hasNextPage: Boolean
475497
hasPreviousPage: Boolean
@@ -504,7 +526,8 @@ export interface User extends BaseGraphQLObject {
504526
bigIntField?: Int | null
505527
jsonField?: JSONObject | null
506528
jsonFieldNoFilter?: JSONObject | null
507-
stringField: String
529+
typedJsonField?: EventObject | null
530+
stringField?: String | null
508531
noFilterField?: String | null
509532
noSortField?: String | null
510533
noFilterOrSortField?: String | null

examples/02-complex-example/generated/classes.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import { BaseWhereInput, JsonObject, PaginationArgs, DateOnlyString, DateTimeStr
2323

2424
import { StringEnum } from "../src/modules/user/user.model";
2525

26+
// @ts-ignore
27+
import { EventParam } from "../src/modules/user/user.model";
28+
// @ts-ignore
29+
import { EventObject } from "../src/modules/user/user.model";
2630
// @ts-ignore
2731
import { User } from "../src/modules/user/user.model";
2832

@@ -787,8 +791,11 @@ export class UserCreateInput {
787791
@TypeGraphQLField(() => GraphQLJSONObject, { nullable: true })
788792
jsonFieldNoFilter?: JsonObject;
789793

790-
@TypeGraphQLField()
791-
stringField!: string;
794+
@TypeGraphQLField(() => EventObject, { nullable: true })
795+
typedJsonField?: EventObject;
796+
797+
@TypeGraphQLField({ nullable: true })
798+
stringField?: string;
792799

793800
@TypeGraphQLField({ nullable: true })
794801
noFilterField?: string;
@@ -913,6 +920,9 @@ export class UserUpdateInput {
913920
@TypeGraphQLField(() => GraphQLJSONObject, { nullable: true })
914921
jsonFieldNoFilter?: JsonObject;
915922

923+
@TypeGraphQLField(() => EventObject, { nullable: true })
924+
typedJsonField?: EventObject;
925+
916926
@TypeGraphQLField({ nullable: true })
917927
stringField?: string;
918928

examples/02-complex-example/generated/schema.graphql

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,26 @@ interface DeleteResponse {
7171
id: ID!
7272
}
7373

74+
type EventObject {
75+
params: [EventParam!]!
76+
}
77+
78+
input EventObjectInput {
79+
params: [EventParamInput!]!
80+
}
81+
82+
type EventParam {
83+
type: String!
84+
name: String!
85+
value: JSONObject!
86+
}
87+
88+
input EventParamInput {
89+
type: String!
90+
name: String!
91+
value: JSONObject!
92+
}
93+
7494
"""
7595
The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
7696
"""
@@ -126,9 +146,10 @@ type User implements BaseGraphQLObject {
126146
bigIntField: Int
127147
jsonField: JSONObject
128148
jsonFieldNoFilter: JSONObject
149+
typedJsonField: EventObject
129150

130151
"""This is a string field"""
131-
stringField: String!
152+
stringField: String
132153
noFilterField: String
133154
noSortField: String
134155
noFilterOrSortField: String
@@ -171,7 +192,8 @@ input UserCreateInput {
171192
bigIntField: Float
172193
jsonField: JSONObject
173194
jsonFieldNoFilter: JSONObject
174-
stringField: String!
195+
typedJsonField: EventObjectInput
196+
stringField: String
175197
noFilterField: String
176198
noSortField: String
177199
noFilterOrSortField: String
@@ -286,6 +308,7 @@ input UserUpdateInput {
286308
bigIntField: Float
287309
jsonField: JSONObject
288310
jsonFieldNoFilter: JSONObject
311+
typedJsonField: EventObjectInput
289312
stringField: String
290313
noFilterField: String
291314
noSortField: String

examples/02-complex-example/src/modules/user/user.model.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// eslint-disable-next-line @typescript-eslint/no-var-requires
2+
const { GraphQLJSONObject } = require('graphql-type-json');
3+
import { Field, InputType } from 'type-graphql';
14
import { Column, Unique } from 'typeorm';
25
import {
36
BaseModel,
@@ -16,6 +19,7 @@ import {
1619
JsonObject,
1720
Model,
1821
NumericField,
22+
ObjectType,
1923
StringField,
2024
FloatField
2125
} from '../../../../../src';
@@ -27,6 +31,26 @@ export enum StringEnum {
2731
BAR = 'BAR'
2832
}
2933

34+
@InputType('EventParamInput')
35+
@ObjectType()
36+
export class EventParam {
37+
@Field()
38+
type!: string;
39+
40+
@Field()
41+
name?: string;
42+
43+
@Field(() => GraphQLJSONObject)
44+
value!: JsonObject;
45+
}
46+
47+
@InputType('EventObjectInput')
48+
@ObjectType()
49+
export class EventObject {
50+
@Field(() => [EventParam])
51+
params!: EventParam[];
52+
}
53+
3054
@Model()
3155
@Unique(['stringField', 'enumField'])
3256
export class User extends BaseModel {
@@ -81,10 +105,13 @@ export class User extends BaseModel {
81105
@JSONField({ filter: false, nullable: true })
82106
jsonFieldNoFilter?: JsonObject;
83107

108+
@JSONField({ filter: false, nullable: true, gqlFieldType: EventObject })
109+
typedJsonField?: EventObject;
110+
84111
@StringField({
85112
maxLength: 50,
86113
minLength: 2,
87-
nullable: false,
114+
nullable: true,
88115
description: 'This is a string field'
89116
})
90117
stringField: string;

examples/02-complex-example/tools/seed.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ async function seedDatabase() {
6565
emailField,
6666
stringField,
6767
jsonField,
68+
typedJsonField: {
69+
params: [
70+
{
71+
name: 'Foo',
72+
type: 'Bar',
73+
value: {
74+
one: 1,
75+
two: 'TWO'
76+
}
77+
}
78+
]
79+
},
6880
dateField,
6981
dateOnlyField,
7082
dateTimeField,

src/decorators/JSONField.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22
const { GraphQLJSONObject } = require('graphql-type-json');
33

44
import { composeMethodDecorators } from '../utils';
5+
import { ClassType } from '../core/types';
56

67
import { getCombinedDecorator } from './getCombinedDecorator';
78

89
interface JSONFieldOptions {
910
nullable?: boolean;
1011
filter?: boolean;
12+
gqlFieldType?: ClassType;
1113
}
1214

1315
export function JSONField(options: JSONFieldOptions = {}): any {
1416
const factories = getCombinedDecorator({
1517
fieldType: 'json',
1618
warthogColumnMeta: options,
17-
gqlFieldType: GraphQLJSONObject,
19+
gqlFieldType: options.gqlFieldType ?? GraphQLJSONObject,
1820
dbType: 'jsonb'
1921
});
2022

src/decorators/ObjectType.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const caller = require('caller'); // eslint-disable-line @typescript-eslint/no-var-requires
2+
import * as path from 'path';
3+
import { ObjectType as TypeGraphQLObjectType } from 'type-graphql';
4+
import { ObjectOptions } from 'type-graphql/dist/decorators/ObjectType.d';
5+
6+
import { ClassType } from '../core';
7+
import { getMetadataStorage } from '../metadata';
8+
import { ClassDecoratorFactory, composeClassDecorators, generatedFolderPath } from '../utils/';
9+
10+
// Allow default TypeORM and TypeGraphQL options to be used
11+
// export function Model({ api = {}, db = {}, apiOnly = false, dbOnly = false }: ModelOptions = {}) {
12+
export function ObjectType(options: ObjectOptions = {}) {
13+
// In order to use the enums in the generated classes file, we need to
14+
// save their locations and import them in the generated file
15+
const modelFileName = caller();
16+
17+
// Use relative paths when linking source files so that we can check the generated code in
18+
// and it will work in any directory structure
19+
const relativeFilePath = path.relative(generatedFolderPath(), modelFileName);
20+
21+
const registerModelWithWarthog = (target: ClassType): void => {
22+
// Save off where the model is located so that we can import it in the generated classes
23+
getMetadataStorage().addClass(target.name, target, relativeFilePath);
24+
};
25+
26+
const factories: any[] = [];
27+
28+
// We add our own Warthog decorator regardless of dbOnly and apiOnly
29+
factories.push(registerModelWithWarthog as ClassDecoratorFactory);
30+
31+
// We shouldn't add this as it creates the GraphQL type, but there is a
32+
// bug if we don't add it because we end up adding the Field decorators in the models
33+
factories.push(TypeGraphQLObjectType(options as ObjectOptions) as ClassDecoratorFactory);
34+
35+
return composeClassDecorators(...factories);
36+
}

src/decorators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from './ManyToManyJoin';
1818
export * from './ManyToOne';
1919
export * from './Model';
2020
export * from './NumericField';
21+
export * from './ObjectType';
2122
export * from './OneToMany';
2223
export * from './StringField';
2324
export * from './UserId';

src/metadata/metadata-storage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ColumnMetadata extends DecoratorCommonOptions {
3535
propertyName: string;
3636
dataType?: ColumnType; // int16, jsonb, etc...
3737
default?: any;
38+
gqlFieldType?: Function;
3839
enum?: GraphQLEnumType;
3940
enumName?: string;
4041
unique?: boolean;
@@ -159,6 +160,17 @@ export class MetadataStorage {
159160
];
160161
}
161162

163+
// Adds a class so that we can import it into classes.ts
164+
// This is typically used when adding a strongly typed JSON column
165+
// using JSONField with a gqlFieldType
166+
addClass(name: string, klass: any, filename: string) {
167+
this.classMap[name] = {
168+
filename,
169+
klass,
170+
name
171+
};
172+
}
173+
162174
addModel(name: string, klass: any, filename: string, options: Partial<ModelMetadata> = {}) {
163175
if (this.interfaces.indexOf(name) > -1) {
164176
return; // Don't add interface types to model list

src/schema/TypeORMConverter.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { ColumnMetadata, getMetadataStorage, ModelMetadata } from '../metadata';
55

66
import {
77
columnToGraphQLType,
8-
columnTypeToGraphQLDataType,
9-
columnInfoToTypeScriptType
8+
columnToGraphQLDataType,
9+
columnToTypeScriptType
1010
} from './type-conversion';
1111
import { WhereOperator } from '../torm';
1212

@@ -36,14 +36,6 @@ export function filenameToImportPath(filename: string): string {
3636
return filename.replace(/\.(j|t)s$/, '').replace(/\\/g, '/');
3737
}
3838

39-
export function columnToGraphQLDataType(column: ColumnMetadata): string {
40-
return columnTypeToGraphQLDataType(column.type, column.enumName);
41-
}
42-
43-
export function columnToTypeScriptType(column: ColumnMetadata): string {
44-
return columnInfoToTypeScriptType(column.type, column.enumName);
45-
}
46-
4739
export function generateEnumMapImports(): string[] {
4840
const imports: string[] = [];
4941
const enumMap = getMetadataStorage().enumMap;
@@ -261,7 +253,7 @@ export function entityToUpdateInputArgs(model: ModelMetadata): string {
261253
}
262254

263255
function columnToTypes(column: ColumnMetadata) {
264-
const graphqlType = columnToGraphQLType(column.type, column.enumName);
256+
const graphqlType = columnToGraphQLType(column);
265257
const tsType = columnToTypeScriptType(column);
266258

267259
return { graphqlType, tsType };
@@ -469,7 +461,7 @@ export function entityToWhereInput(model: ModelMetadata): string {
469461
}
470462
} else if (column.type === 'json') {
471463
fieldTemplates += `
472-
@TypeGraphQLField(() => GraphQLJSONObject, { nullable: true })
464+
@TypeGraphQLField(() => ${graphQLDataType}, { nullable: true })
473465
${column.propertyName}_json?: JsonObject;
474466
`;
475467
}

0 commit comments

Comments
 (0)