@smartive/graphql-magic 14.0.2 → 15.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 (41) hide show
  1. package/.env +1 -1
  2. package/CHANGELOG.md +1 -6
  3. package/dist/bin/gqm.cjs +132 -121
  4. package/dist/cjs/index.cjs +134 -119
  5. package/dist/esm/client/queries.d.ts +1 -1
  6. package/dist/esm/client/queries.js +1 -1
  7. package/dist/esm/client/queries.js.map +1 -1
  8. package/dist/esm/context.d.ts +1 -1
  9. package/dist/esm/models/utils.d.ts +4 -4
  10. package/dist/esm/permissions/check.d.ts +1 -0
  11. package/dist/esm/permissions/check.js +19 -11
  12. package/dist/esm/permissions/check.js.map +1 -1
  13. package/dist/esm/resolvers/filters.js +1 -1
  14. package/dist/esm/resolvers/filters.js.map +1 -1
  15. package/dist/esm/resolvers/mutations.js +4 -4
  16. package/dist/esm/resolvers/mutations.js.map +1 -1
  17. package/dist/esm/resolvers/resolver.js +3 -0
  18. package/dist/esm/resolvers/resolver.js.map +1 -1
  19. package/dist/esm/resolvers/resolvers.d.ts +1 -1
  20. package/dist/esm/resolvers/resolvers.js +29 -23
  21. package/dist/esm/resolvers/resolvers.js.map +1 -1
  22. package/dist/esm/resolvers/selects.js +4 -3
  23. package/dist/esm/resolvers/selects.js.map +1 -1
  24. package/dist/esm/schema/generate.js +76 -72
  25. package/dist/esm/schema/generate.js.map +1 -1
  26. package/docker-compose.yml +0 -4
  27. package/package.json +6 -6
  28. package/src/bin/gqm/codegen.ts +3 -1
  29. package/src/bin/gqm/gqm.ts +3 -69
  30. package/src/bin/gqm/parse-knexfile.ts +5 -6
  31. package/src/bin/gqm/settings.ts +29 -3
  32. package/src/bin/gqm/templates.ts +70 -8
  33. package/src/client/queries.ts +2 -2
  34. package/src/context.ts +1 -1
  35. package/src/permissions/check.ts +24 -16
  36. package/src/resolvers/filters.ts +1 -1
  37. package/src/resolvers/mutations.ts +4 -4
  38. package/src/resolvers/resolver.ts +4 -0
  39. package/src/resolvers/resolvers.ts +33 -27
  40. package/src/resolvers/selects.ts +4 -3
  41. package/src/schema/generate.ts +78 -72
@@ -13,11 +13,10 @@ import {
13
13
  printSchemaFromModels,
14
14
  } from '../..';
15
15
  import { generateGraphqlApiTypes, generateGraphqlClientTypes } from './codegen';
16
- import { KNEXFILE_PATH, parseKnexfile } from './parse-knexfile';
16
+ import { parseKnexfile } from './parse-knexfile';
17
17
  import { parseModels } from './parse-models';
18
18
  import { readLine } from './readline';
19
- import { ensureFileExists, getSetting, getSettings, writeToFile } from './settings';
20
- import { KNEXFILE } from './templates';
19
+ import { getSetting, writeToFile } from './settings';
21
20
 
22
21
  config({
23
22
  path: '.env',
@@ -28,18 +27,11 @@ config({
28
27
 
29
28
  program.description('The graphql-magic cli.');
30
29
 
31
- program
32
- .command('setup')
33
- .description('Set up the project')
34
- .action(async () => {
35
- await getSettings();
36
- ensureFileExists(KNEXFILE_PATH, KNEXFILE);
37
- });
38
-
39
30
  program
40
31
  .command('generate')
41
32
  .description('Generate all the things')
42
33
  .action(async () => {
34
+ await getSetting('knexfilePath');
43
35
  const models = await parseModels();
44
36
  const generatedFolderPath = await getSetting('generatedFolderPath');
45
37
  const gqlModule = await getSetting('gqlModule');
@@ -51,64 +43,6 @@ program
51
43
  await generateGraphqlClientTypes();
52
44
  });
53
45
 
54
- program
55
- .command('generate-models')
56
- .description('Generate models.json')
57
- .action(async () => {
58
- await parseModels();
59
- });
60
-
61
- program
62
- .command('generate-schema')
63
- .description('Generate schema')
64
- .action(async () => {
65
- const models = await parseModels();
66
- const generatedFolderPath = await getSetting('generatedFolderPath');
67
- writeToFile(`${generatedFolderPath}/schema.graphql`, printSchemaFromModels(models));
68
- });
69
-
70
- program
71
- .command('generate-mutation-queries')
72
- .description('Generate mutation-queries')
73
- .action(async () => {
74
- const models = await parseModels();
75
- const generatedFolderPath = await getSetting('generatedFolderPath');
76
- const gqlModule = await getSetting('gqlModule');
77
- writeToFile(`${generatedFolderPath}/client/mutations.ts`, generateMutations(models, gqlModule));
78
- });
79
-
80
- program
81
- .command('generate-db-types')
82
- .description('Generate DB types')
83
- .action(async () => {
84
- const models = await parseModels();
85
- const generatedFolderPath = await getSetting('generatedFolderPath');
86
- writeToFile(`${generatedFolderPath}/db/index.ts`, generateDBModels(models));
87
- });
88
-
89
- program
90
- .command('generate-knex-types')
91
- .description('Generate Knex types')
92
- .action(async () => {
93
- const models = await parseModels();
94
- const generatedFolderPath = await getSetting('generatedFolderPath');
95
- writeToFile(`${generatedFolderPath}/db/knex.ts`, generateKnexTables(models));
96
- });
97
-
98
- program
99
- .command('generate-graphql-api-types')
100
- .description('Generate Graphql API types')
101
- .action(async () => {
102
- await generateGraphqlApiTypes();
103
- });
104
-
105
- program
106
- .command('generate-graphql-client-types')
107
- .description('Generate Graphql client types')
108
- .action(async () => {
109
- await generateGraphqlClientTypes();
110
- });
111
-
112
46
  program
113
47
  .command('generate-migration [<name>] [<date>]')
114
48
  .description('Generate Migration')
@@ -1,21 +1,20 @@
1
1
  import { IndentationText, Project } from 'ts-morph';
2
- import { ensureFileExists } from './settings';
2
+ import { ensureFileExists, getSetting } from './settings';
3
3
  import { staticEval } from './static-eval';
4
4
  import { KNEXFILE } from './templates';
5
5
  import { findDeclarationInFile } from './utils';
6
6
 
7
- export const KNEXFILE_PATH = `knexfile.ts`;
8
-
9
7
  export const parseKnexfile = async () => {
10
8
  const project = new Project({
11
9
  manipulationSettings: {
12
10
  indentationText: IndentationText.TwoSpaces,
13
11
  },
14
12
  });
15
- ensureFileExists(KNEXFILE_PATH, KNEXFILE);
13
+ const knexfilePath = await getSetting('knexfilePath');
14
+ ensureFileExists(knexfilePath, KNEXFILE);
16
15
 
17
- const sourceFile = project.addSourceFileAtPath(KNEXFILE_PATH);
18
- const configDeclaration = findDeclarationInFile(sourceFile, 'config');
16
+ const sourceFile = project.addSourceFileAtPath(knexfilePath);
17
+ const configDeclaration = findDeclarationInFile(sourceFile, 'knexConfig');
19
18
  const config = staticEval(configDeclaration, {});
20
19
  return config;
21
20
  };
@@ -1,11 +1,28 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { dirname } from 'path';
3
3
  import { readLine } from './readline';
4
- import { EMPTY_MODELS } from './templates';
4
+ import { EMPTY_MODELS, GET_ME, GITIGNORE, KNEXFILE } from './templates';
5
5
 
6
6
  const SETTINGS_PATH = '.gqmrc.json';
7
7
 
8
+ const DEFAULT_ENV = {
9
+ DATABASE_HOST: 'localhost',
10
+ DATABASE_NAME: 'postgres',
11
+ DATABASE_USER: 'postgres',
12
+ DATABASE_PASSWORD: 'password',
13
+ };
14
+
8
15
  const DEFAULTS = {
16
+ knexfilePath: {
17
+ question: 'What is the knexfile path?',
18
+ defaultValue: 'knexfile.ts',
19
+ init: (path: string) => {
20
+ for (const [name, value] of Object.entries(DEFAULT_ENV)) {
21
+ ensureFileContains('.env', `${name}=`, `${name}=${value}\n`);
22
+ }
23
+ ensureFileExists(path, KNEXFILE);
24
+ },
25
+ },
9
26
  modelsPath: {
10
27
  question: 'What is the models path?',
11
28
  defaultValue: 'src/config/models.ts',
@@ -17,6 +34,7 @@ const DEFAULTS = {
17
34
  question: 'What is the path for generated stuff?',
18
35
  defaultValue: 'src/generated',
19
36
  init: (path: string) => {
37
+ ensureFileContains('.gitignore', GITIGNORE(path));
20
38
  ensureFileExists(`${path}/.gitkeep`, '');
21
39
  ensureFileExists(`${path}/db/.gitkeep`, '');
22
40
  ensureFileExists(`${path}/api/.gitkeep`, '');
@@ -27,7 +45,7 @@ const DEFAULTS = {
27
45
  question: 'Where to look for graphql queries?',
28
46
  defaultValue: 'src/graphql/client/queries',
29
47
  init: (path: string) => {
30
- ensureDirectoryExists(path);
48
+ ensureFileExists(`${path}/get-me.ts`, GET_ME);
31
49
  },
32
50
  },
33
51
  gqlModule: {
@@ -80,7 +98,7 @@ export const getSetting = async (name: keyof Settings): Promise<string> => {
80
98
  return settings[name];
81
99
  };
82
100
 
83
- const ensureDirectoryExists = (dir: string) => {
101
+ export const ensureDirectoryExists = (dir: string) => {
84
102
  if (existsSync(dir)) {
85
103
  return true;
86
104
  }
@@ -107,6 +125,14 @@ export const ensureFileExists = (filePath: string, content: string) => {
107
125
  }
108
126
  };
109
127
 
128
+ export const ensureFileContains = (filePath: string, content: string, fallback?: string) => {
129
+ ensureFileExists(filePath, content);
130
+ const fileContent = readFileSync(filePath, 'utf-8');
131
+ if (!fileContent.includes(content)) {
132
+ writeFileSync(filePath, fileContent + (fallback ?? content));
133
+ }
134
+ };
135
+
110
136
  export const writeToFile = (filePath: string, content: string) => {
111
137
  ensureDirectoryExists(dirname(filePath));
112
138
  if (existsSync(filePath)) {
@@ -1,7 +1,19 @@
1
- export const EMPTY_MODELS = `
2
- import { RawModels, Models } from '@smartive/graphql-magic';
1
+ export const GITIGNORE = (path: string) => `
2
+ # graphql-magic
3
+ ${path}/**/*
4
+ ${path}/**/.gitkeep
5
+ `;
6
+
7
+ export const DOTENV = `
8
+ DATABASE_HOST=localhost
9
+ DATABASE_NAME=postgres
10
+ DATABASE_USER=postgres
11
+ DATABASE_PASSWORD=password
12
+ `;
13
+
14
+ export const EMPTY_MODELS = `import { ModelDefinitions, Models } from '@smartive/graphql-magic';
3
15
 
4
- const rawModels: RawModels = [
16
+ const modelDefinitions: ModelDefinitions = [
5
17
  {
6
18
  kind: 'entity',
7
19
  name: 'User',
@@ -9,11 +21,10 @@ const rawModels: RawModels = [
9
21
  },
10
22
  ]
11
23
 
12
- export const models = new Models(rawModels);
24
+ export const models = new Models(modelDefinitions);
13
25
  `;
14
26
 
15
- export const KNEXFILE = `
16
- import { DateTime } from 'luxon';
27
+ export const KNEXFILE = `import { DateTime } from 'luxon';
17
28
  import { types } from 'pg';
18
29
 
19
30
  const dateOids = { date: 1082, timestamptz: 1184, timestamp: 1114 };
@@ -26,7 +37,7 @@ for (const oid of Object.values(numberOids)) {
26
37
  types.setTypeParser(oid, Number);
27
38
  }
28
39
 
29
- const config = {
40
+ const knexConfig = {
30
41
  client: 'postgresql',
31
42
  connection: {
32
43
  host: process.env.DATABASE_HOST,
@@ -43,5 +54,56 @@ const config = {
43
54
  },
44
55
  } as const;
45
56
 
46
- export default config;
57
+ export default knexConfig;
58
+ `;
59
+
60
+ export const GET_ME = `import { gql } from '@smartive/graphql-magic';
61
+
62
+ export const GET_ME = gql\`
63
+ query GetMe {
64
+ me {
65
+ id
66
+ }
67
+ }
68
+ \`;
69
+ `;
70
+
71
+ export const EXECUTE = `
72
+ import knexConfig from "@/knexfile";
73
+ import { getSession } from "@auth0/nextjs-auth0";
74
+ import { Context, User, execute } from "@smartive/graphql-magic";
75
+ import { randomUUID } from "crypto";
76
+ import { knex } from 'knex';
77
+ import { DateTime } from "luxon";
78
+ import { models } from "../config/models";
79
+
80
+ export const executeGraphql = async <T, V = undefined>(
81
+ body: {
82
+ query: string;
83
+ operationName?: string;
84
+ variables?: V;
85
+ options?: { email?: string };
86
+ }): Promise<{ data: T }> => {
87
+ const session = await getSession();
88
+
89
+ const db = knex(knexConfig);
90
+ let user: User | undefined;
91
+ // TODO: get user
92
+
93
+ const result = await execute({
94
+ req: null as unknown as Context['req'],
95
+ body,
96
+ knex: db as unknown as Context['knex'],
97
+ locale: 'en',
98
+ locales: ['en'],
99
+ user,
100
+ models: models,
101
+ permissions: { ADMIN: true, UNAUTHENTICATED: true }, // TODO: fine-grained permissions
102
+ now: DateTime.local(),
103
+ });
104
+ await db.destroy();
105
+
106
+ // https://github.com/vercel/next.js/issues/47447#issuecomment-1500371732
107
+ return JSON.parse(JSON.stringify(result)) as { data: T };
108
+ }
47
109
  `;
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  export const getUpdateEntityQuery = (
16
16
  model: EntityModel,
17
- role: any,
17
+ role: string,
18
18
  fields?: string[] | undefined,
19
19
  additionalFields = ''
20
20
  ) => `query Update${model.name}Fields ($id: ID!) {
@@ -28,7 +28,7 @@ export const getUpdateEntityQuery = (
28
28
  .join(' ')}
29
29
  ${getActionableRelations(model, 'update')
30
30
  .filter((name) => !fields || fields.includes(name))
31
- .map((name) => `${name} { id }`)}
31
+ .map((name) => `${name} { id, display: ${model.getRelation(name).targetModel.displayField || 'id'} }`)}
32
32
  ${additionalFields}
33
33
  }
34
34
  }`;
package/src/context.ts CHANGED
@@ -17,7 +17,7 @@ export type Context = {
17
17
  document: DocumentNode;
18
18
  locale: string;
19
19
  locales: string[];
20
- user: User;
20
+ user?: User;
21
21
  models: Models;
22
22
  permissions: Permissions;
23
23
  mutationHook?: MutationHook;
@@ -7,12 +7,14 @@ import { AliasGenerator, hash, ors } from '../resolvers/utils';
7
7
  import { BasicValue } from '../values';
8
8
  import { PermissionAction, PermissionLink, PermissionStack } from './generate';
9
9
 
10
+ export const getRole = (ctx: Pick<FullContext, 'user'>) => ctx.user?.role ?? 'UNAUTHENTICATED';
11
+
10
12
  export const getPermissionStack = (
11
13
  ctx: Pick<FullContext, 'permissions' | 'user'>,
12
14
  type: string,
13
15
  action: PermissionAction
14
16
  ): boolean | PermissionStack => {
15
- const rolePermissions = ctx.permissions[ctx.user.role];
17
+ const rolePermissions = ctx.permissions[getRole(ctx)];
16
18
  if (typeof rolePermissions === 'boolean' || rolePermissions === undefined) {
17
19
  return !!rolePermissions;
18
20
  }
@@ -45,7 +47,7 @@ export const applyPermissions = (
45
47
  }
46
48
 
47
49
  if (permissionStack === false) {
48
- console.error(`No applicable permissions exist for ${ctx.user.role} ${type} ${action}.`);
50
+ console.error(`No applicable permissions exist for ${getRole(ctx)} ${type} ${action}.`);
49
51
  // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
50
52
  query.where(false);
51
53
  return permissionStack;
@@ -113,7 +115,7 @@ export const getEntityToMutate = async (
113
115
  .map(([key, value]) => `${key}: ${value}`)
114
116
  .join(', ')}`
115
117
  );
116
- throw new PermissionError(ctx.user.role, action, `this ${model.name}`, 'no available permissions applied');
118
+ throw new PermissionError(getRole(ctx), action, `this ${model.name}`, 'no available permissions applied');
117
119
  }
118
120
 
119
121
  if (model.parent) {
@@ -139,7 +141,7 @@ export const checkCanWrite = async (
139
141
  return;
140
142
  }
141
143
  if (permissionStack === false) {
142
- throw new PermissionError(ctx.user.role, action, model.plural, 'no applicable permissions');
144
+ throw new PermissionError(getRole(ctx), action, model.plural, 'no applicable permissions');
143
145
  }
144
146
 
145
147
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- using `select(1 as any)` to instantiate an "empty" query builder
@@ -156,13 +158,9 @@ export const checkCanWrite = async (
156
158
  }
157
159
 
158
160
  const fieldPermissions = field[action === 'CREATE' ? 'creatable' : 'updatable'];
159
- if (fieldPermissions && typeof fieldPermissions === 'object' && !fieldPermissions.roles?.includes(ctx.user.role)) {
160
- throw new PermissionError(
161
- ctx.user.role,
162
- action,
163
- `this ${model.name}'s ${field.name}`,
164
- 'field permission not available'
165
- );
161
+ const role = getRole(ctx);
162
+ if (fieldPermissions && typeof fieldPermissions === 'object' && !fieldPermissions.roles?.includes(role)) {
163
+ throw new PermissionError(role, action, `this ${model.name}'s ${field.name}`, 'field permission not available');
166
164
  }
167
165
 
168
166
  linked = true;
@@ -178,7 +176,7 @@ export const checkCanWrite = async (
178
176
 
179
177
  if (fieldPermissionStack === false || !fieldPermissionStack.length) {
180
178
  throw new PermissionError(
181
- ctx.user.role,
179
+ role,
182
180
  action,
183
181
  `this ${model.name}'s ${field.name}`,
184
182
  'no applicable permissions on data to link'
@@ -194,13 +192,14 @@ export const checkCanWrite = async (
194
192
  );
195
193
  }
196
194
 
195
+ const role = getRole(ctx);
197
196
  if (linked) {
198
197
  const canMutate = await query;
199
198
  if (!canMutate) {
200
- throw new PermissionError(ctx.user.role, action, `this ${model.name}`, 'no linkable entities');
199
+ throw new PermissionError(role, action, `this ${model.name}`, 'no linkable entities');
201
200
  }
202
201
  } else if (action === 'CREATE') {
203
- throw new PermissionError(ctx.user.role, action, `this ${model.name}`, 'no linkable entities');
202
+ throw new PermissionError(role, action, `this ${model.name}`, 'no linkable entities');
204
203
  }
205
204
  };
206
205
 
@@ -213,12 +212,21 @@ const permissionLinkQuery = (
213
212
  const aliases = new AliasGenerator();
214
213
  let alias = aliases.getShort();
215
214
  const { type, me, where } = links[0];
216
- // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
217
- subQuery.from(`${type} as ${alias}`);
215
+
218
216
  if (me) {
217
+ if (!ctx.user) {
218
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
219
+ subQuery.where(false);
220
+ return;
221
+ }
222
+
219
223
  // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
220
224
  subQuery.where({ [`${alias}.id`]: ctx.user.id });
221
225
  }
226
+
227
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
228
+ subQuery.from(`${type} as ${alias}`);
229
+
222
230
  if (where) {
223
231
  applyWhere(ctx.models.getModel(type, 'entity'), subQuery, alias, where, aliases);
224
232
  }
@@ -38,7 +38,7 @@ export const applyFilters = (node: FieldResolverNode, query: Knex.QueryBuilder,
38
38
  normalizedArguments.where.deleted &&
39
39
  (!Array.isArray(normalizedArguments.where.deleted) || normalizedArguments.where.deleted.some((v) => v))
40
40
  ) {
41
- if (node.ctx.user.role !== 'ADMIN') {
41
+ if (node.ctx.user?.role !== 'ADMIN') {
42
42
  throw new ForbiddenError('You cannot access deleted entries.');
43
43
  }
44
44
  } else {
@@ -32,7 +32,7 @@ const create = async (model: EntityModel, { data: input }: { data: any }, ctx: F
32
32
  const normalizedInput = { ...input };
33
33
  normalizedInput.id = uuid();
34
34
  normalizedInput.createdAt = ctx.now;
35
- normalizedInput.createdById = ctx.user.id;
35
+ normalizedInput.createdById = ctx.user?.id;
36
36
  if (model.parent) {
37
37
  normalizedInput.type = model.name;
38
38
  }
@@ -165,7 +165,7 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
165
165
  toDelete[currentModel.name][entity.id as string] = (entity[currentModel.displayField || 'id'] || entity.id) as string;
166
166
 
167
167
  if (!dryRun) {
168
- const normalizedInput = { deleted: true, deletedAt: ctx.now, deletedById: ctx.user.id };
168
+ const normalizedInput = { deleted: true, deletedAt: ctx.now, deletedById: ctx.user?.id };
169
169
  const data = { prev: entity, input: {}, normalizedInput, next: { ...entity, ...normalizedInput } };
170
170
  if (ctx.mutationHook) {
171
171
  beforeHooks.push(async () => {
@@ -319,7 +319,7 @@ const createRevision = async (model: EntityModel, data: Entity, ctx: Context) =>
319
319
  id: revisionId,
320
320
  [`${typeToField(model.parent || model.name)}Id`]: data.id,
321
321
  createdAt: ctx.now,
322
- createdById: ctx.user.id,
322
+ createdById: ctx.user?.id,
323
323
  };
324
324
 
325
325
  if (model.deletable) {
@@ -354,7 +354,7 @@ const createRevision = async (model: EntityModel, data: Entity, ctx: Context) =>
354
354
  const sanitize = (ctx: FullContext, model: EntityModel, data: Entity) => {
355
355
  if (model.updatable) {
356
356
  data.updatedAt = ctx.now;
357
- data.updatedById = ctx.user.id;
357
+ data.updatedById = ctx.user?.id;
358
358
  }
359
359
 
360
360
  for (const key of Object.keys(data)) {
@@ -29,6 +29,10 @@ export const resolve = async (ctx: FullContext, id?: string) => {
29
29
  const { query, verifiedPermissionStacks } = await buildQuery(node);
30
30
 
31
31
  if (ctx.info.fieldName === 'me') {
32
+ if (!node.ctx.user.id) {
33
+ return undefined;
34
+ }
35
+
32
36
  void query.where({ [getColumn(node, 'id')]: node.ctx.user.id });
33
37
  }
34
38
 
@@ -3,23 +3,25 @@ import { isRootModel, merge, not, typeToField } from '../models/utils';
3
3
  import { mutationResolver } from './mutations';
4
4
  import { queryResolver } from './resolver';
5
5
 
6
- export const getResolvers = (models: Models) => ({
7
- Query: merge([
8
- {
9
- me: queryResolver,
10
- },
11
- ...models.entities
12
- .filter(({ queriable }) => queriable)
13
- .map((model) => ({
14
- [typeToField(model.name)]: queryResolver,
15
- })),
16
- ...models.entities
17
- .filter(({ listQueriable }) => listQueriable)
18
- .map((model) => ({
19
- [model.pluralField]: queryResolver,
20
- })),
21
- ]),
22
- Mutation: merge<unknown>([
6
+ export const getResolvers = (models: Models) => {
7
+ const resolvers: Record<string, any> = {
8
+ Query: merge([
9
+ {
10
+ me: queryResolver,
11
+ },
12
+ ...models.entities
13
+ .filter(({ queriable }) => queriable)
14
+ .map((model) => ({
15
+ [typeToField(model.name)]: queryResolver,
16
+ })),
17
+ ...models.entities
18
+ .filter(({ listQueriable }) => listQueriable)
19
+ .map((model) => ({
20
+ [model.pluralField]: queryResolver,
21
+ })),
22
+ ]),
23
+ };
24
+ const mutations = [
23
25
  ...models.entities
24
26
  .filter(not(isRootModel))
25
27
  .filter(({ creatable }) => creatable)
@@ -39,13 +41,17 @@ export const getResolvers = (models: Models) => ({
39
41
  [`delete${model.name}`]: mutationResolver,
40
42
  [`restore${model.name}`]: mutationResolver,
41
43
  })),
42
- ]),
43
- ...Object.assign(
44
- {},
45
- ...models.entities.filter(isRootModel).map((model) => ({
46
- [model.name]: {
47
- __resolveType: ({ TYPE }) => TYPE,
48
- },
49
- }))
50
- ),
51
- });
44
+ ];
45
+
46
+ if (mutations.length) {
47
+ resolvers.Mutation = merge<unknown>(mutations);
48
+ }
49
+
50
+ for (const model of models.entities.filter(isRootModel)) {
51
+ resolvers[model.name] = {
52
+ __resolveType: ({ TYPE }) => TYPE,
53
+ };
54
+ }
55
+
56
+ return resolvers;
57
+ };
@@ -11,7 +11,7 @@ import {
11
11
  getNameOrAlias,
12
12
  getSimpleFields,
13
13
  } from '.';
14
- import { PermissionError, UserInputError } from '..';
14
+ import { PermissionError, UserInputError, getRole } from '..';
15
15
 
16
16
  export const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins: Joins) => {
17
17
  // Simple field selects
@@ -28,9 +28,10 @@ export const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins
28
28
  return false;
29
29
  }
30
30
 
31
- if (typeof field.queriable === 'object' && !field.queriable.roles?.includes(node.ctx.user.role)) {
31
+ const role = getRole(node.ctx);
32
+ if (typeof field.queriable === 'object' && !field.queriable.roles?.includes(role)) {
32
33
  throw new PermissionError(
33
- node.ctx.user.role,
34
+ role,
34
35
  'READ',
35
36
  `${node.model.name}'s field "${field.name}"`,
36
37
  'field permission not available'