@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.
- package/.env +1 -1
- package/CHANGELOG.md +1 -6
- package/dist/bin/gqm.cjs +132 -121
- package/dist/cjs/index.cjs +134 -119
- package/dist/esm/client/queries.d.ts +1 -1
- package/dist/esm/client/queries.js +1 -1
- package/dist/esm/client/queries.js.map +1 -1
- package/dist/esm/context.d.ts +1 -1
- package/dist/esm/models/utils.d.ts +4 -4
- package/dist/esm/permissions/check.d.ts +1 -0
- package/dist/esm/permissions/check.js +19 -11
- package/dist/esm/permissions/check.js.map +1 -1
- package/dist/esm/resolvers/filters.js +1 -1
- package/dist/esm/resolvers/filters.js.map +1 -1
- package/dist/esm/resolvers/mutations.js +4 -4
- package/dist/esm/resolvers/mutations.js.map +1 -1
- package/dist/esm/resolvers/resolver.js +3 -0
- package/dist/esm/resolvers/resolver.js.map +1 -1
- package/dist/esm/resolvers/resolvers.d.ts +1 -1
- package/dist/esm/resolvers/resolvers.js +29 -23
- package/dist/esm/resolvers/resolvers.js.map +1 -1
- package/dist/esm/resolvers/selects.js +4 -3
- package/dist/esm/resolvers/selects.js.map +1 -1
- package/dist/esm/schema/generate.js +76 -72
- package/dist/esm/schema/generate.js.map +1 -1
- package/docker-compose.yml +0 -4
- package/package.json +6 -6
- package/src/bin/gqm/codegen.ts +3 -1
- package/src/bin/gqm/gqm.ts +3 -69
- package/src/bin/gqm/parse-knexfile.ts +5 -6
- package/src/bin/gqm/settings.ts +29 -3
- package/src/bin/gqm/templates.ts +70 -8
- package/src/client/queries.ts +2 -2
- package/src/context.ts +1 -1
- package/src/permissions/check.ts +24 -16
- package/src/resolvers/filters.ts +1 -1
- package/src/resolvers/mutations.ts +4 -4
- package/src/resolvers/resolver.ts +4 -0
- package/src/resolvers/resolvers.ts +33 -27
- package/src/resolvers/selects.ts +4 -3
- package/src/schema/generate.ts +78 -72
package/src/bin/gqm/gqm.ts
CHANGED
|
@@ -13,11 +13,10 @@ import {
|
|
|
13
13
|
printSchemaFromModels,
|
|
14
14
|
} from '../..';
|
|
15
15
|
import { generateGraphqlApiTypes, generateGraphqlClientTypes } from './codegen';
|
|
16
|
-
import {
|
|
16
|
+
import { parseKnexfile } from './parse-knexfile';
|
|
17
17
|
import { parseModels } from './parse-models';
|
|
18
18
|
import { readLine } from './readline';
|
|
19
|
-
import {
|
|
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
|
-
|
|
13
|
+
const knexfilePath = await getSetting('knexfilePath');
|
|
14
|
+
ensureFileExists(knexfilePath, KNEXFILE);
|
|
16
15
|
|
|
17
|
-
const sourceFile = project.addSourceFileAtPath(
|
|
18
|
-
const configDeclaration = findDeclarationInFile(sourceFile, '
|
|
16
|
+
const sourceFile = project.addSourceFileAtPath(knexfilePath);
|
|
17
|
+
const configDeclaration = findDeclarationInFile(sourceFile, 'knexConfig');
|
|
19
18
|
const config = staticEval(configDeclaration, {});
|
|
20
19
|
return config;
|
|
21
20
|
};
|
package/src/bin/gqm/settings.ts
CHANGED
|
@@ -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
|
-
|
|
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)) {
|
package/src/bin/gqm/templates.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
`;
|
package/src/client/queries.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
export const getUpdateEntityQuery = (
|
|
16
16
|
model: EntityModel,
|
|
17
|
-
role:
|
|
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
package/src/permissions/check.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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(
|
|
199
|
+
throw new PermissionError(role, action, `this ${model.name}`, 'no linkable entities');
|
|
201
200
|
}
|
|
202
201
|
} else if (action === 'CREATE') {
|
|
203
|
-
throw new PermissionError(
|
|
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
|
-
|
|
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
|
}
|
package/src/resolvers/filters.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
};
|
package/src/resolvers/selects.ts
CHANGED
|
@@ -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
|
-
|
|
31
|
+
const role = getRole(node.ctx);
|
|
32
|
+
if (typeof field.queriable === 'object' && !field.queriable.roles?.includes(role)) {
|
|
32
33
|
throw new PermissionError(
|
|
33
|
-
|
|
34
|
+
role,
|
|
34
35
|
'READ',
|
|
35
36
|
`${node.model.name}'s field "${field.name}"`,
|
|
36
37
|
'field permission not available'
|