@smartive/graphql-magic 3.0.1 → 4.0.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +1 -6
  2. package/dist/cjs/index.cjs +205 -172
  3. package/dist/esm/client/queries.d.ts +4 -1
  4. package/dist/esm/client/queries.js +16 -6
  5. package/dist/esm/client/queries.js.map +1 -1
  6. package/dist/esm/db/generate.js +21 -13
  7. package/dist/esm/db/generate.js.map +1 -1
  8. package/dist/esm/generate/generate.js +17 -22
  9. package/dist/esm/generate/generate.js.map +1 -1
  10. package/dist/esm/generate/utils.d.ts +10 -1
  11. package/dist/esm/generate/utils.js.map +1 -1
  12. package/dist/esm/migrations/generate.js +82 -89
  13. package/dist/esm/migrations/generate.js.map +1 -1
  14. package/dist/esm/models.d.ts +115 -93
  15. package/dist/esm/models.js +1 -26
  16. package/dist/esm/models.js.map +1 -1
  17. package/dist/esm/permissions/check.js +2 -2
  18. package/dist/esm/permissions/check.js.map +1 -1
  19. package/dist/esm/permissions/generate.js +2 -2
  20. package/dist/esm/permissions/generate.js.map +1 -1
  21. package/dist/esm/resolvers/filters.js +1 -1
  22. package/dist/esm/resolvers/filters.js.map +1 -1
  23. package/dist/esm/resolvers/mutations.js +5 -6
  24. package/dist/esm/resolvers/mutations.js.map +1 -1
  25. package/dist/esm/resolvers/node.js +4 -5
  26. package/dist/esm/resolvers/node.js.map +1 -1
  27. package/dist/esm/resolvers/resolver.js +2 -2
  28. package/dist/esm/resolvers/resolver.js.map +1 -1
  29. package/dist/esm/utils.d.ts +181 -1
  30. package/dist/esm/utils.js +69 -21
  31. package/dist/esm/utils.js.map +1 -1
  32. package/package.json +8 -8
  33. package/src/client/queries.ts +26 -12
  34. package/src/db/generate.ts +22 -15
  35. package/src/generate/generate.ts +26 -34
  36. package/src/generate/utils.ts +11 -1
  37. package/src/migrations/generate.ts +84 -82
  38. package/src/models.ts +113 -157
  39. package/src/permissions/check.ts +2 -2
  40. package/src/permissions/generate.ts +2 -2
  41. package/src/resolvers/filters.ts +1 -1
  42. package/src/resolvers/mutations.ts +10 -9
  43. package/src/resolvers/node.ts +6 -6
  44. package/src/resolvers/resolver.ts +2 -2
  45. package/src/utils.ts +158 -57
  46. package/tests/unit/__snapshots__/generate.spec.ts.snap +3 -0
  47. package/tests/unit/__snapshots__/queries.spec.ts.snap +11 -0
  48. package/tests/unit/queries.spec.ts +10 -0
  49. package/tests/utils/database/schema.ts +2 -0
  50. package/tests/utils/models.ts +14 -6
@@ -1,10 +1,11 @@
1
1
  import { GraphQLResolveInfo } from 'graphql';
2
+ import { DateTime } from 'luxon';
2
3
  import { v4 as uuid } from 'uuid';
3
4
  import { Context, FullContext } from '../context';
4
5
  import { ForbiddenError, GraphQLError } from '../errors';
5
- import { Entity, Model, ModelField, isEnumList } from '../models';
6
+ import { Entity, Model, ModelField } from '../models';
6
7
  import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
7
- import { get, it, summonByName, typeToField } from '../utils';
8
+ import { get, isEnumList, it, summonByName, typeToField } from '../utils';
8
9
  import { resolve } from './resolver';
9
10
  import { AliasGenerator } from './utils';
10
11
 
@@ -113,10 +114,10 @@ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolea
113
114
  if (!(currentModel.name in toDelete)) {
114
115
  toDelete[currentModel.name] = {};
115
116
  }
116
- if (entity.id in toDelete[currentModel.name]) {
117
+ if ((entity.id as string) in toDelete[currentModel.name]) {
117
118
  return;
118
119
  }
119
- toDelete[currentModel.name][entity.id] = entity[currentModel.displayField || 'id'] || entity.id;
120
+ toDelete[currentModel.name][entity.id as string] = (entity[currentModel.displayField || 'id'] || entity.id) as string;
120
121
 
121
122
  if (!dryRun) {
122
123
  const normalizedInput = { deleted: true, deletedAt: ctx.now, deletedById: ctx.user.id };
@@ -275,8 +276,8 @@ const createRevision = async (model: Model, data: Entity, ctx: Context) => {
275
276
  revisionData.deleted = data.deleted || false;
276
277
  }
277
278
 
278
- for (const { name, relation, nonNull, ...field } of model.fields.filter(({ updatable }) => updatable)) {
279
- const col = relation ? `${name}Id` : name;
279
+ for (const { type, name, nonNull, ...field } of model.fields.filter(({ updatable }) => updatable)) {
280
+ const col = type === 'relation' ? `${name}Id` : name;
280
281
  if (nonNull && (!(col in data) || col === undefined || col === null)) {
281
282
  revisionData[col] = get(field, 'default');
282
283
  } else {
@@ -301,16 +302,16 @@ const sanitize = (ctx: FullContext, model: Model, data: Entity) => {
301
302
  }
302
303
 
303
304
  if (isEndOfDay(field) && data[key]) {
304
- data[key] = data[key].endOf('day');
305
+ data[key] = (data[key] as DateTime).endOf('day');
305
306
  continue;
306
307
  }
307
308
 
308
309
  if (isEnumList(ctx.rawModels, field) && Array.isArray(data[key])) {
309
- data[key] = `{${data[key].join(',')}}`;
310
+ data[key] = `{${(data[key] as string[]).join(',')}}`;
310
311
  continue;
311
312
  }
312
313
  }
313
314
  };
314
315
 
315
316
  const isEndOfDay = (field?: ModelField) =>
316
- field?.endOfDay === true && field?.dateTimeType === 'date' && field?.type === 'DateTime';
317
+ field.type === 'DateTime' && field?.endOfDay === true && field?.dateTimeType === 'date' && field?.type === 'DateTime';
@@ -8,8 +8,8 @@ import type {
8
8
  } from 'graphql';
9
9
 
10
10
  import { FullContext } from '../context';
11
- import { isJsonObjectModel, Model } from '../models';
12
- import { get, summonByKey, summonByName } from '../utils';
11
+ import { Model } from '../models';
12
+ import { get, isRawObjectModel, summonByKey, summonByName } from '../utils';
13
13
  import {
14
14
  getFragmentTypeName,
15
15
  getNameOrAlias,
@@ -113,7 +113,7 @@ export const getSimpleFields = (node: ResolverNode) => {
113
113
  return true;
114
114
  }
115
115
 
116
- return node.model.fields.some(({ json, name }) => json && name === selection.name.value);
116
+ return node.model.fields.some(({ type, name }) => type === 'json' && name === selection.name.value);
117
117
  });
118
118
  };
119
119
 
@@ -150,13 +150,13 @@ export const getJoins = (node: ResolverNode, toMany: boolean) => {
150
150
 
151
151
  const typeName = getTypeName(fieldDefinition.type);
152
152
 
153
- if (isJsonObjectModel(summonByName(ctx.rawModels, typeName))) {
153
+ if (isRawObjectModel(summonByName(ctx.rawModels, typeName))) {
154
154
  continue;
155
155
  }
156
156
 
157
157
  const baseModel = summonByName(ctx.models, baseTypeDefinition.name.value);
158
158
 
159
- let foreignKey;
159
+ let foreignKey: string | undefined;
160
160
  if (toMany) {
161
161
  const reverseRelation = baseModel.reverseRelationsByName[fieldName];
162
162
  if (!reverseRelation) {
@@ -165,7 +165,7 @@ export const getJoins = (node: ResolverNode, toMany: boolean) => {
165
165
  foreignKey = reverseRelation.foreignKey;
166
166
  } else {
167
167
  const modelField = baseModel.fieldsByName[fieldName];
168
- if (!modelField || modelField.raw) {
168
+ if (modelField?.type !== 'relation') {
169
169
  continue;
170
170
  }
171
171
  foreignKey = modelField.foreignKey;
@@ -115,11 +115,11 @@ const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins: Joins
115
115
  .filter((n) => {
116
116
  const field = node.model.fields.find(({ name }) => name === n.name.value);
117
117
 
118
- if (!field || field.relation || field.raw) {
118
+ if (!field || field.type === 'relation' || field.type === 'raw') {
119
119
  return false;
120
120
  }
121
121
 
122
- if (field.queriableBy && !field.queriableBy.includes(node.ctx.user.role)) {
122
+ if (typeof field.queriable === 'object' && !field.queriable.roles?.includes(node.ctx.user.role)) {
123
123
  throw new PermissionError(
124
124
  'READ',
125
125
  `${node.model.name}'s field "${field.name}"`,
package/src/utils.ts CHANGED
@@ -4,7 +4,24 @@ import camelCase from 'lodash/camelCase';
4
4
  import lodashGet from 'lodash/get';
5
5
  import kebabCase from 'lodash/kebabCase';
6
6
  import startCase from 'lodash/startCase';
7
- import { Model, Models, ObjectModel, RawModels, Relation, ReverseRelation, isObjectModel } from './models';
7
+ import {
8
+ BooleanField,
9
+ DateTimeField,
10
+ EnumModel,
11
+ Model,
12
+ ModelField,
13
+ Models,
14
+ ObjectModel,
15
+ RawEnumModel,
16
+ RawField,
17
+ RawModel,
18
+ RawModels,
19
+ RawObjectModel,
20
+ Relation,
21
+ RelationField,
22
+ ReverseRelation,
23
+ ScalarModel,
24
+ } from './models';
8
25
 
9
26
  const isNotFalsy = <T>(v: T | null | undefined | false): v is T => typeof v !== 'undefined' && v !== null && v !== false;
10
27
 
@@ -26,6 +43,59 @@ export const getModelLabel = (model: Model) => getLabel(model.name);
26
43
 
27
44
  export const getLabel = (s: string) => startCase(camelCase(s));
28
45
 
46
+ export const isObjectModel = (model: RawModel): model is ObjectModel => model.type === 'object';
47
+
48
+ export const isEnumModel = (model: RawModel): model is EnumModel => model.type === 'enum';
49
+
50
+ export const isRawEnumModel = (model: RawModel): model is RawEnumModel => model.type === 'raw-enum';
51
+
52
+ export const isScalarModel = (model: RawModel): model is ScalarModel => model.type === 'scalar';
53
+
54
+ export const isRawObjectModel = (model: RawModel): model is RawObjectModel => model.type === 'raw';
55
+
56
+ export const isEnumList = (models: RawModels, field: ModelField) =>
57
+ field?.list === true && models.find(({ name }) => name === field.type)?.type === 'enum';
58
+
59
+ export const and =
60
+ (...predicates: ((field: ModelField) => boolean)[]) =>
61
+ (field: ModelField) =>
62
+ predicates.every((predicate) => predicate(field));
63
+
64
+ export const not = (predicate: (field: ModelField) => boolean) => (field: ModelField) => !predicate(field);
65
+
66
+ export const isRelation = (field: ModelField): field is RelationField => field.type === 'relation';
67
+
68
+ export const isToOneRelation = (field: ModelField): field is RelationField => isRelation(field) && !!field.toOne;
69
+
70
+ export const isQueriableField = ({ queriable }: ModelField) => queriable !== false;
71
+
72
+ export const isRaw = (field: ModelField): field is RawField => field.type === 'raw';
73
+
74
+ export const isVisible = ({ hidden }: ModelField) => hidden !== true;
75
+
76
+ export const isSimpleField = and(not(isRelation), not(isRaw));
77
+
78
+ export const isUpdatable = ({ updatable }: ModelField) => !!updatable;
79
+
80
+ export const isCreatable = ({ creatable }: ModelField) => !!creatable;
81
+
82
+ export const isQueriableBy = (role: string) => (field: ModelField) =>
83
+ field.queriable !== false && (field.queriable == true || !field.queriable.roles || field.queriable.roles.includes(role));
84
+
85
+ export const isUpdatableBy = (role: string) => (field: ModelField) =>
86
+ field.updatable && (field.updatable === true || !field.updatable.roles || field.updatable.roles.includes(role));
87
+
88
+ export const isCreatableBy = (role: string) => (field: ModelField) =>
89
+ field.creatable && (field.creatable === true || !field.creatable.roles || field.creatable.roles.includes(role));
90
+
91
+ export const actionableRelations = (model: Model, action: 'create' | 'update' | 'filter') =>
92
+ model.fields
93
+ .filter(isRelation)
94
+ .filter(
95
+ (field) =>
96
+ field[`${action === 'filter' ? action : action.slice(0, -1)}able` as 'filterable' | 'creatable' | 'updatable']
97
+ );
98
+
29
99
  export const getModels = (rawModels: RawModels): Models => {
30
100
  const models: Models = rawModels.filter(isObjectModel).map((model) => {
31
101
  const objectModel: Model = {
@@ -35,60 +105,86 @@ export const getModels = (rawModels: RawModels): Models => {
35
105
  relationsByName: {},
36
106
  reverseRelations: [],
37
107
  reverseRelationsByName: {},
38
- fields: [
39
- { name: 'id', type: 'ID', nonNull: true, unique: true, primary: true, generated: true },
40
- ...model.fields,
41
- ...(model.creatable
42
- ? [
43
- { name: 'createdAt', type: 'DateTime', nonNull: !model.nonStrict, orderable: true, generated: true },
44
- {
45
- name: 'createdBy',
46
- type: 'User',
47
- relation: true,
48
- nonNull: !model.nonStrict,
49
- reverse: `created${getModelPlural(model)}`,
50
- generated: true,
51
- },
52
- ]
53
- : []),
54
- ...(model.updatable
55
- ? [
56
- { name: 'updatedAt', type: 'DateTime', nonNull: !model.nonStrict, orderable: true, generated: true },
57
- {
58
- name: 'updatedBy',
59
- type: 'User',
60
- relation: true,
61
- nonNull: !model.nonStrict,
62
- reverse: `updated${getModelPlural(model)}`,
63
- generated: true,
64
- },
65
- ]
66
- : []),
67
- ...(model.deletable
68
- ? [
69
- {
70
- name: 'deleted',
71
- type: 'Boolean',
72
- nonNull: true,
73
- default: false,
74
- filterable: true,
75
- defaultFilter: false,
76
- generated: true,
77
- },
78
- { name: 'deletedAt', type: 'DateTime', orderable: true, generated: true },
79
- {
80
- name: 'deletedBy',
81
- type: 'User',
82
- relation: true,
83
- reverse: `deleted${getModelPlural(model)}`,
84
- generated: true,
85
- },
86
- ]
87
- : []),
88
- ].map(({ foreignKey, ...field }) => ({
108
+ fields: (
109
+ [
110
+ { name: 'id', type: 'ID', nonNull: true, unique: true, primary: true, generated: true },
111
+ ...model.fields,
112
+ ...(model.creatable
113
+ ? [
114
+ {
115
+ name: 'createdAt',
116
+ type: 'DateTime',
117
+
118
+ nonNull: true,
119
+ orderable: true,
120
+ generated: true,
121
+ ...(typeof model.creatable === 'object' && model.creatable.createdAt),
122
+ } satisfies DateTimeField,
123
+ {
124
+ name: 'createdBy',
125
+ type: 'relation',
126
+ typeName: 'User',
127
+ nonNull: true,
128
+ reverse: `created${getModelPlural(model)}`,
129
+ generated: true,
130
+ ...(typeof model.creatable === 'object' && model.creatable.createdBy),
131
+ } satisfies RelationField,
132
+ ]
133
+ : []),
134
+ ...(model.updatable
135
+ ? [
136
+ {
137
+ name: 'updatedAt',
138
+ type: 'DateTime',
139
+ nonNull: true,
140
+ orderable: true,
141
+ generated: true,
142
+ ...(typeof model.updatable === 'object' && model.updatable.updatedAt),
143
+ } satisfies DateTimeField,
144
+ {
145
+ name: 'updatedBy',
146
+ type: 'relation',
147
+ typeName: 'User',
148
+ nonNull: true,
149
+ reverse: `updated${getModelPlural(model)}`,
150
+ generated: true,
151
+ ...(typeof model.updatable === 'object' && model.updatable.updatedBy),
152
+ } satisfies RelationField,
153
+ ]
154
+ : []),
155
+ ...(model.deletable
156
+ ? [
157
+ {
158
+ name: 'deleted',
159
+ type: 'Boolean',
160
+ nonNull: true,
161
+ default: false,
162
+ filterable: { default: false },
163
+ generated: true,
164
+ ...(typeof model.deletable === 'object' && model.deletable.deleted),
165
+ } satisfies BooleanField,
166
+ {
167
+ name: 'deletedAt',
168
+ type: 'DateTime',
169
+ orderable: true,
170
+ generated: true,
171
+ ...(typeof model.deletable === 'object' && model.deletable.deletedAt),
172
+ } satisfies DateTimeField,
173
+ {
174
+ name: 'deletedBy',
175
+ type: 'relation',
176
+ typeName: 'User',
177
+ reverse: `deleted${getModelPlural(model)}`,
178
+ generated: true,
179
+ ...(typeof model.deletable === 'object' && model.deletable.deletedBy),
180
+ } satisfies RelationField,
181
+ ]
182
+ : []),
183
+ ] satisfies ModelField[]
184
+ ).map((field: ModelField) => ({
89
185
  ...field,
90
- ...(field.relation && {
91
- foreignKey: foreignKey || `${field.name}Id`,
186
+ ...(field.type === 'relation' && {
187
+ foreignKey: field.foreignKey || `${field.name}Id`,
92
188
  }),
93
189
  })),
94
190
  };
@@ -101,13 +197,18 @@ export const getModels = (rawModels: RawModels): Models => {
101
197
  });
102
198
 
103
199
  for (const model of models) {
104
- for (const field of model.fields.filter(({ relation }) => relation)) {
105
- const fieldModel = summonByName(models, field.type);
200
+ for (const field of model.fields) {
201
+ if (field.type !== 'relation') {
202
+ continue;
203
+ }
204
+
205
+ const fieldModel = summonByName(models, field.typeName);
106
206
 
107
207
  const reverseRelation: ReverseRelation = {
208
+ type: 'relation',
108
209
  name: field.reverse || (field.toOne ? typeToField(model.name) : getModelPluralField(model)),
109
210
  foreignKey: get(field, 'foreignKey'),
110
- type: model.name,
211
+ typeName: model.name,
111
212
  toOne: !!field.toOne,
112
213
  fieldModel,
113
214
  field,
@@ -3,6 +3,7 @@
3
3
  exports[`generate generates a schema 1`] = `
4
4
  "type AnotherObject {
5
5
  id: ID!
6
+ name: String
6
7
  myself: AnotherObject
7
8
  deleted: Boolean!
8
9
  deletedAt: DateTime
@@ -12,6 +13,7 @@ exports[`generate generates a schema 1`] = `
12
13
  }
13
14
 
14
15
  input AnotherObjectOrderBy {
16
+ name: Order
15
17
  deletedAt: Order
16
18
  }
17
19
 
@@ -99,6 +101,7 @@ type SomeRawObject {
99
101
  }
100
102
 
101
103
  input UpdateSomeObject {
104
+ anotherId: ID
102
105
  xyz: Int
103
106
  }
104
107
 
@@ -0,0 +1,11 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`queries getEntityRelationsQuery applies filters 1`] = `
4
+ "query UpdateSomeObjectRelations {
5
+ another: anotherObjects(orderBy: [{ name: ASC }]) {
6
+ id
7
+ display: name
8
+
9
+ }
10
+ }"
11
+ `;
@@ -0,0 +1,10 @@
1
+ import { getEditEntityRelationsQuery, summonByName } from "../../src";
2
+ import { models } from "../utils/models";
3
+
4
+ describe('queries', () => {
5
+ describe('getEntityRelationsQuery', () => {
6
+ it('applies filters', () => {
7
+ expect(getEditEntityRelationsQuery(models, summonByName(models, 'SomeObject'), 'update')).toMatchSnapshot()
8
+ });
9
+ })
10
+ })
@@ -54,6 +54,8 @@ export const setupSchema = async (knex: Knex) => {
54
54
  await knex.schema.createTable('SomeObjectRevision', (table) => {
55
55
  table.uuid('id').notNullable().primary();
56
56
  table.uuid('someObjectId').notNullable();
57
+ table.uuid('anotherId').notNullable();
58
+ table.foreign('anotherId').references('id').inTable('AnotherObject');
57
59
  table.uuid('createdById').notNullable();
58
60
  table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now(0));
59
61
  table.boolean('deleted').notNullable();
@@ -16,7 +16,7 @@ export const rawModels: RawModels = [
16
16
 
17
17
  {
18
18
  name: 'SomeRawObject',
19
- type: 'raw-object',
19
+ type: 'raw',
20
20
  fields: [{ name: 'field', type: 'String' }],
21
21
  },
22
22
 
@@ -30,7 +30,8 @@ export const rawModels: RawModels = [
30
30
  },
31
31
  {
32
32
  name: 'role',
33
- type: 'Role',
33
+ type: 'enum',
34
+ typeName: 'Role',
34
35
  },
35
36
  ],
36
37
  },
@@ -39,13 +40,19 @@ export const rawModels: RawModels = [
39
40
  name: 'AnotherObject',
40
41
  listQueriable: true,
41
42
  deletable: true,
43
+ displayField: 'name',
42
44
  fields: [
43
45
  {
44
- type: 'AnotherObject',
46
+ type: 'String',
47
+ name: 'name',
48
+ orderable: true,
49
+ },
50
+ {
51
+ type: 'relation',
52
+ typeName: 'AnotherObject',
45
53
  name: 'myself',
46
54
  toOne: true,
47
55
  reverse: 'self',
48
- relation: true
49
56
  }
50
57
  ],
51
58
  },
@@ -67,9 +74,10 @@ export const rawModels: RawModels = [
67
74
  },
68
75
  {
69
76
  name: 'another',
70
- type: 'AnotherObject',
71
- relation: true,
77
+ type: 'relation',
78
+ typeName: 'AnotherObject',
72
79
  filterable: true,
80
+ updatable: true,
73
81
  nonNull: true,
74
82
  },
75
83
  {