@platformatic/sql-graphql 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -274,6 +274,7 @@ function constructGraph (app, entity, opts, ignore) {
274
274
  entity,
275
275
  loadMany,
276
276
  getFields,
277
+ relationalFields,
277
278
  fields
278
279
  }
279
280
 
@@ -20,19 +20,20 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
20
20
 
21
21
  assert(table_name, `table_name is required ${enhanceAssertLogMsg}`)
22
22
  assert(foreign_table_name, `foreign_table_name is required ${enhanceAssertLogMsg}`)
23
- assert(column_name.toLowerCase().endsWith('id'), `The column with id reference must be ended with "id" postfix ${enhanceAssertLogMsg}`)
24
23
 
25
24
  const current = tablesTypeMap[table_name]
26
25
  const foreign = tablesTypeMap[foreign_table_name]
27
26
  assert(foreign !== undefined, `No foreign table named "${foreign_table_name}" was found ${enhanceAssertLogMsg}`)
28
27
 
29
- // current to foreign
30
- {
31
- const lowered = lowerCaseFirst(camelcase(cutOutIdEnding(column_name)))
28
+ // current to foreign, we skip this if the foreign table has a composite primary key
29
+ // TODO implement support for this case
30
+ /* istanbul ignore else */
31
+ if (foreign.entity.primaryKeys.size === 1) {
32
+ const lowered = generateRelationshipName(column_name, foreign)
32
33
  if (!relationships[current.type] || relationships[current.type][lowered] !== false) {
33
- current.fields[lowered] = { type: foreign.type }
34
34
  const originalField = camelcase(column_name)
35
35
  delete current.fields[originalField]
36
+ current.fields[lowered] = { type: foreign.type }
36
37
  loaders[current.type] = loaders[current.type] || resolvers[current.type] || {}
37
38
  const key = camelcase(foreign_column_name)
38
39
  loaders[current.type][lowered] = {
@@ -59,7 +60,7 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
59
60
  resolvers[foreign.type] = resolvers[foreign.type] || {}
60
61
  const resolveRelation = async function (obj, args, ctx, info) {
61
62
  const fields = fromSelectionSet(info.fieldNodes[0].selectionSet, new Set())
62
- const toSearch = { ...args, fields: [...fields, column_name], ctx }
63
+ const toSearch = { ...args, fields: [...fields, column_name, ...current.relationalFields], ctx }
63
64
  toSearch.where = toSearch.where || {}
64
65
  toSearch.where[camelcase(column_name)] = { eq: obj.id }
65
66
  return current.entity.find(toSearch)
@@ -84,9 +85,13 @@ function lowerCaseFirst (str) {
84
85
  return str.charAt(0).toLowerCase() + str.slice(1)
85
86
  }
86
87
 
87
- function cutOutIdEnding (str) {
88
+ function generateRelationshipName (str, foreign) {
89
+ const primaryKey = foreign.entity.primaryKeys.values().next().value
88
90
  str = str.toString()
89
- return str.slice(0, str.toLowerCase().lastIndexOf('id'))
91
+ if (str.endsWith('_' + primaryKey)) {
92
+ str = str.slice(0, -2)
93
+ }
94
+ return lowerCaseFirst(camelcase(str))
90
95
  }
91
96
 
92
97
  function findPrimaryColumnName (camelCasedFields) {
package/lib/utils.js CHANGED
@@ -21,11 +21,13 @@ function sqlTypeToGraphQL (sqlType) {
21
21
  case 'decimal':
22
22
  return graphql.GraphQLInt
23
23
  case 'bigint':
24
- return graphql.GraphQLInt
24
+ return graphql.GraphQLString
25
25
  case 'int2':
26
26
  return graphql.GraphQLInt
27
27
  case 'int4':
28
28
  return graphql.GraphQLInt
29
+ case 'int8':
30
+ return graphql.GraphQLString
29
31
  case 'varchar':
30
32
  return graphql.GraphQLString
31
33
  case 'text':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/sql-graphql",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "Map SQL dbs to GraphQL",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -14,8 +14,8 @@
14
14
  },
15
15
  "homepage": "https://github.com/platformatic/platformatic#readme",
16
16
  "devDependencies": {
17
- "@platformatic/sql-events": "0.9.2",
18
- "@platformatic/sql-mapper": "0.9.2",
17
+ "@platformatic/sql-events": "0.10.0",
18
+ "@platformatic/sql-mapper": "0.10.0",
19
19
  "fastify": "^4.6.0",
20
20
  "snazzy": "^9.0.0",
21
21
  "standard": "^17.0.0",
@@ -722,3 +722,82 @@ test('[SQLite] - UUID', { skip: !isSQLite }, async ({ pass, teardown, same, equa
722
722
  }, 'savePage response')
723
723
  }
724
724
  })
725
+
726
+ test('BIGINT!', { skip: isSQLite, only: true }, async ({ pass, teardown, same, equal }) => {
727
+ const app = fastify()
728
+ app.register(sqlMapper, {
729
+ ...connInfo,
730
+ async onDatabaseLoad (db, sql) {
731
+ pass('onDatabaseLoad called')
732
+
733
+ await clear(db, sql)
734
+
735
+ await db.query(sql`
736
+ CREATE TABLE simple_types (
737
+ id SERIAL PRIMARY KEY,
738
+ counter BIGINT
739
+ );`)
740
+ }
741
+ })
742
+ teardown(app.close.bind(app))
743
+
744
+ app.register(sqlGraphQL)
745
+
746
+ await app.ready()
747
+
748
+ const counter = BigInt(Number.MAX_SAFE_INTEGER) + 1000n
749
+
750
+ {
751
+ const res = await app.inject({
752
+ method: 'POST',
753
+ url: '/graphql',
754
+ body: {
755
+ query: `
756
+ mutation {
757
+ saveSimpleType(input: {
758
+ counter: "${counter}"
759
+ }) {
760
+ id
761
+ counter
762
+ }
763
+ }
764
+ `
765
+ }
766
+ })
767
+ equal(res.statusCode, 200, 'saveSimpleType status code')
768
+ same(res.json(), {
769
+ data: {
770
+ saveSimpleType: {
771
+ id: '1',
772
+ counter
773
+ }
774
+ }
775
+ }, 'saveSimpleType response')
776
+ }
777
+
778
+ {
779
+ const res = await app.inject({
780
+ method: 'POST',
781
+ url: '/graphql',
782
+ body: {
783
+ query: `
784
+ query {
785
+ getSimpleTypeById(id: 1) {
786
+ id
787
+ counter
788
+ }
789
+ }
790
+ `
791
+ }
792
+ })
793
+ equal(res.statusCode, 200, 'getSimpleTypeById status code')
794
+ same(res.json(), {
795
+ data: {
796
+ getSimpleTypeById: {
797
+ id: 1,
798
+ counter
799
+ }
800
+ }
801
+ }, 'getSimpleTypeById response')
802
+ }
803
+ })
@@ -0,0 +1,172 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const sqlGraphQL = require('..')
5
+ const sqlMapper = require('@platformatic/sql-mapper')
6
+ const fastify = require('fastify')
7
+ const { connInfo, isPg } = require('./helper')
8
+
9
+ async function runMigrations (db, sql) {
10
+ await db.query(sql`
11
+ create table company (
12
+ id SERIAL primary key,
13
+ name varchar(60)
14
+ );
15
+
16
+ create table social_medias (
17
+ id SERIAL primary key,
18
+ name varchar(60)
19
+ );
20
+
21
+ create table company_social_medias (
22
+ id SERIAL primary key,
23
+ username varchar not null,
24
+ social_medias_id integer not null,
25
+ company_id integer not null,
26
+ constraint "fk_company_id" foreign key ("company_id") references company,
27
+ constraint "fk_social_medias_id" foreign key ("social_medias_id") references social_medias
28
+ );
29
+ `)
30
+ }
31
+
32
+ async function clean (db, sql) {
33
+ await db.query(sql`
34
+ drop table if exists company_social_medias;
35
+ drop table if exists company;
36
+ drop table if exists social_medias;
37
+ `)
38
+ }
39
+
40
+ test('do not crash', { skip: !isPg }, async ({ pass, teardown, same, equal }) => {
41
+ const app = fastify()
42
+ app.register(sqlMapper, {
43
+ ...connInfo,
44
+ async onDatabaseLoad (db, sql) {
45
+ pass('onDatabaseLoad called')
46
+ await clean(db, sql)
47
+
48
+ await runMigrations(db, sql)
49
+ teardown(async () => {
50
+ await clean(db, sql)
51
+ })
52
+ }
53
+ })
54
+ await app.register(sqlGraphQL)
55
+ teardown(app.close.bind(app))
56
+
57
+ await app.ready()
58
+
59
+ {
60
+ const res = await app.inject({
61
+ method: 'POST',
62
+ url: '/graphql',
63
+ body: {
64
+ query: `
65
+ mutation {
66
+ saveCompany(input: { name: "Platformatic" }) {
67
+ id
68
+ name
69
+ }
70
+ }
71
+ `
72
+ }
73
+ })
74
+ equal(res.statusCode, 200, 'saveCompany status code')
75
+ same(res.json(), {
76
+ data: {
77
+ saveCompany: {
78
+ id: 1,
79
+ name: 'Platformatic'
80
+ }
81
+ }
82
+ }, 'saveCompany response')
83
+ }
84
+
85
+ {
86
+ const res = await app.inject({
87
+ method: 'POST',
88
+ url: '/graphql',
89
+ body: {
90
+ query: `
91
+ mutation {
92
+ saveSocialMedia(input: { name: "Twitter" }) {
93
+ id
94
+ name
95
+ }
96
+ }
97
+ `
98
+ }
99
+ })
100
+ equal(res.statusCode, 200, 'saveSocialMedia status code')
101
+ same(res.json(), {
102
+ data: {
103
+ saveSocialMedia: {
104
+ id: 1,
105
+ name: 'Twitter'
106
+ }
107
+ }
108
+ }, 'saveSocialMedia response')
109
+ }
110
+
111
+ {
112
+ const res = await app.inject({
113
+ method: 'POST',
114
+ url: '/graphql',
115
+ body: {
116
+ query: `
117
+ mutation {
118
+ saveCompanySocialMedia(input: { companyId: 1, socialMediasId: 1, username: "platformatic" }) {
119
+ id
120
+ username
121
+ }
122
+ }
123
+ `
124
+ }
125
+ })
126
+ equal(res.statusCode, 200, 'saveCompanySocialMedia status code')
127
+ same(res.json(), {
128
+ data: {
129
+ saveCompanySocialMedia: {
130
+ id: 1,
131
+ username: 'platformatic'
132
+ }
133
+ }
134
+ }, 'saveCompanySocialMedia response')
135
+ }
136
+
137
+ {
138
+ const res = await app.inject({
139
+ method: 'POST',
140
+ url: '/graphql',
141
+ body: {
142
+ query: `
143
+ query {
144
+ company {
145
+ name
146
+ companySocialMedias {
147
+ username
148
+ socialMedias {
149
+ name
150
+ }
151
+ }
152
+ }
153
+ }
154
+ `
155
+ }
156
+ })
157
+ equal(res.statusCode, 200, 'complex status code')
158
+ same(res.json(), {
159
+ data: {
160
+ company: [{
161
+ name: 'Platformatic',
162
+ companySocialMedias: [{
163
+ username: 'platformatic',
164
+ socialMedias: {
165
+ name: 'Twitter'
166
+ }
167
+ }]
168
+ }]
169
+ }
170
+ }, 'complex response')
171
+ }
172
+ })
@@ -860,3 +860,255 @@ test('nested update', async ({ pass, teardown, same, equal }) => {
860
860
  }, 'savePage response')
861
861
  }
862
862
  })
863
+
864
+ test('nested resolver without `id` suffix', async ({ pass, teardown, same, equal }) => {
865
+ const app = fastify()
866
+ app.register(sqlMapper, {
867
+ ...connInfo,
868
+ async onDatabaseLoad (db, sql) {
869
+ pass('onDatabaseLoad called')
870
+
871
+ await clear(db, sql)
872
+
873
+ if (isMysql) {
874
+ await db.query(sql`
875
+ CREATE TABLE categories (
876
+ id SERIAL PRIMARY KEY,
877
+ name VARCHAR(255)
878
+ );
879
+ CREATE TABLE pages (
880
+ id SERIAL PRIMARY KEY,
881
+ title VARCHAR(42),
882
+ category BIGINT UNSIGNED,
883
+ FOREIGN KEY (category) REFERENCES categories(id) ON DELETE CASCADE
884
+ );
885
+ `)
886
+ } else if (isSQLite) {
887
+ await db.query(sql`
888
+ CREATE TABLE categories (
889
+ id INTEGER PRIMARY KEY,
890
+ name VARCHAR(42)
891
+ );
892
+ `)
893
+ await db.query(sql`
894
+ CREATE TABLE pages (
895
+ id INTEGER PRIMARY KEY,
896
+ title VARCHAR(42),
897
+ category INTEGER REFERENCES categories(id)
898
+ );
899
+ `)
900
+ } else {
901
+ await db.query(sql`
902
+ CREATE TABLE categories (
903
+ id SERIAL PRIMARY KEY,
904
+ name VARCHAR(42)
905
+ );
906
+ CREATE TABLE pages (
907
+ id SERIAL PRIMARY KEY,
908
+ title VARCHAR(42),
909
+ category INTEGER REFERENCES categories(id)
910
+ );
911
+ `)
912
+ }
913
+ }
914
+ })
915
+ app.register(sqlGraphQL)
916
+ teardown(app.close.bind(app))
917
+
918
+ const categories = [{
919
+ name: 'Pets'
920
+ }, {
921
+ name: 'Food'
922
+ }]
923
+
924
+ await app.inject({
925
+ method: 'POST',
926
+ url: '/graphql',
927
+ body: {
928
+ query: `
929
+ mutation batch($inputs: [CategoryInput]!) {
930
+ insertCategories(inputs: $inputs) {
931
+ id
932
+ name
933
+ }
934
+ }
935
+ `,
936
+ variables: {
937
+ inputs: categories
938
+ }
939
+ }
940
+ })
941
+
942
+ {
943
+ const res = await app.inject({
944
+ method: 'POST',
945
+ url: '/graphql',
946
+ body: {
947
+ query: `
948
+ mutation {
949
+ savePage(input: { title: "Hello", category: 1 }) {
950
+ id
951
+ title
952
+ category {
953
+ id
954
+ name
955
+ }
956
+ }
957
+ }
958
+ `
959
+ }
960
+ })
961
+ equal(res.statusCode, 200, 'savePage status code')
962
+ same(res.json(), {
963
+ data: {
964
+ savePage: {
965
+ id: 1,
966
+ title: 'Hello',
967
+ category: {
968
+ id: 1,
969
+ name: 'Pets'
970
+ }
971
+ }
972
+ }
973
+ }, 'savePage response')
974
+ }
975
+
976
+ {
977
+ const res = await app.inject({
978
+ method: 'POST',
979
+ url: '/graphql',
980
+ body: {
981
+ query: `
982
+ query {
983
+ getPageById(id: 1) {
984
+ id
985
+ title
986
+ category {
987
+ id
988
+ name
989
+ pages {
990
+ id
991
+ title
992
+ category {
993
+ id
994
+ name
995
+ }
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ `
1001
+ }
1002
+ })
1003
+ equal(res.statusCode, 200, 'pages status code')
1004
+ same(res.json(), {
1005
+ data: {
1006
+ getPageById: {
1007
+ id: 1,
1008
+ title: 'Hello',
1009
+ category: {
1010
+ id: 1,
1011
+ name: 'Pets',
1012
+ pages: [{
1013
+ id: 1,
1014
+ title: 'Hello',
1015
+ category: {
1016
+ id: 1,
1017
+ name: 'Pets'
1018
+ }
1019
+ }]
1020
+ }
1021
+ }
1022
+ }
1023
+ }, 'pages response')
1024
+ }
1025
+
1026
+ // Without ids
1027
+ {
1028
+ const res = await app.inject({
1029
+ method: 'POST',
1030
+ url: '/graphql',
1031
+ body: {
1032
+ query: `
1033
+ query {
1034
+ getPageById(id: 1) {
1035
+ title
1036
+ category {
1037
+ name
1038
+ pages {
1039
+ title
1040
+ category {
1041
+ name
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ `
1048
+ }
1049
+ })
1050
+ equal(res.statusCode, 200, 'pages status code')
1051
+ same(res.json(), {
1052
+ data: {
1053
+ getPageById: {
1054
+ title: 'Hello',
1055
+ category: {
1056
+ name: 'Pets',
1057
+ pages: [{
1058
+ title: 'Hello',
1059
+ category: {
1060
+ name: 'Pets'
1061
+ }
1062
+ }]
1063
+ }
1064
+ }
1065
+ }
1066
+ }, 'pages response')
1067
+ }
1068
+
1069
+ {
1070
+ const res = await app.inject({
1071
+ method: 'POST',
1072
+ url: '/graphql',
1073
+ body: {
1074
+ query: `
1075
+ query {
1076
+ categories {
1077
+ id
1078
+ name
1079
+ pages {
1080
+ id
1081
+ title
1082
+ category {
1083
+ id
1084
+ name
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+ `
1090
+ }
1091
+ })
1092
+ equal(res.statusCode, 200, 'categories.posts status code')
1093
+ same(res.json(), {
1094
+ data: {
1095
+ categories: [{
1096
+ id: 1,
1097
+ name: 'Pets',
1098
+ pages: [{
1099
+ id: 1,
1100
+ title: 'Hello',
1101
+ category: {
1102
+ id: 1,
1103
+ name: 'Pets'
1104
+ }
1105
+ }]
1106
+ }, {
1107
+ id: 2,
1108
+ name: 'Food',
1109
+ pages: []
1110
+ }]
1111
+ }
1112
+ }, 'categories.posts response')
1113
+ }
1114
+ })