@smartive/graphql-magic 1.0.1
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/.eslintrc +21 -0
- package/.github/workflows/release.yml +24 -0
- package/.github/workflows/testing.yml +37 -0
- package/.nvmrc +1 -0
- package/.prettierignore +34 -0
- package/.prettierrc.json +1 -0
- package/.releaserc +27 -0
- package/CHANGELOG.md +6 -0
- package/README.md +15 -0
- package/dist/cjs/index.cjs +2646 -0
- package/dist/esm/client/gql.d.ts +1 -0
- package/dist/esm/client/gql.js +5 -0
- package/dist/esm/client/gql.js.map +1 -0
- package/dist/esm/client/index.d.ts +2 -0
- package/dist/esm/client/index.js +4 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/client/queries.d.ts +24 -0
- package/dist/esm/client/queries.js +152 -0
- package/dist/esm/client/queries.js.map +1 -0
- package/dist/esm/context.d.ts +30 -0
- package/dist/esm/context.js +2 -0
- package/dist/esm/context.js.map +1 -0
- package/dist/esm/errors.d.ts +17 -0
- package/dist/esm/errors.js +27 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/generate/generate.d.ts +7 -0
- package/dist/esm/generate/generate.js +211 -0
- package/dist/esm/generate/generate.js.map +1 -0
- package/dist/esm/generate/index.d.ts +3 -0
- package/dist/esm/generate/index.js +5 -0
- package/dist/esm/generate/index.js.map +1 -0
- package/dist/esm/generate/mutations.d.ts +2 -0
- package/dist/esm/generate/mutations.js +18 -0
- package/dist/esm/generate/mutations.js.map +1 -0
- package/dist/esm/generate/utils.d.ts +22 -0
- package/dist/esm/generate/utils.js +150 -0
- package/dist/esm/generate/utils.js.map +1 -0
- package/dist/esm/index.d.ts +10 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/migrations/generate.d.ts +28 -0
- package/dist/esm/migrations/generate.js +516 -0
- package/dist/esm/migrations/generate.js.map +1 -0
- package/dist/esm/migrations/index.d.ts +1 -0
- package/dist/esm/migrations/index.js +3 -0
- package/dist/esm/migrations/index.js.map +1 -0
- package/dist/esm/models.d.ts +170 -0
- package/dist/esm/models.js +27 -0
- package/dist/esm/models.js.map +1 -0
- package/dist/esm/permissions/check.d.ts +15 -0
- package/dist/esm/permissions/check.js +162 -0
- package/dist/esm/permissions/check.js.map +1 -0
- package/dist/esm/permissions/generate.d.ts +45 -0
- package/dist/esm/permissions/generate.js +77 -0
- package/dist/esm/permissions/generate.js.map +1 -0
- package/dist/esm/permissions/index.d.ts +2 -0
- package/dist/esm/permissions/index.js +4 -0
- package/dist/esm/permissions/index.js.map +1 -0
- package/dist/esm/resolvers/arguments.d.ts +26 -0
- package/dist/esm/resolvers/arguments.js +88 -0
- package/dist/esm/resolvers/arguments.js.map +1 -0
- package/dist/esm/resolvers/filters.d.ts +5 -0
- package/dist/esm/resolvers/filters.js +126 -0
- package/dist/esm/resolvers/filters.js.map +1 -0
- package/dist/esm/resolvers/index.d.ts +7 -0
- package/dist/esm/resolvers/index.js +9 -0
- package/dist/esm/resolvers/index.js.map +1 -0
- package/dist/esm/resolvers/mutations.d.ts +3 -0
- package/dist/esm/resolvers/mutations.js +255 -0
- package/dist/esm/resolvers/mutations.js.map +1 -0
- package/dist/esm/resolvers/node.d.ts +44 -0
- package/dist/esm/resolvers/node.js +102 -0
- package/dist/esm/resolvers/node.js.map +1 -0
- package/dist/esm/resolvers/resolver.d.ts +5 -0
- package/dist/esm/resolvers/resolver.js +143 -0
- package/dist/esm/resolvers/resolver.js.map +1 -0
- package/dist/esm/resolvers/resolvers.d.ts +9 -0
- package/dist/esm/resolvers/resolvers.js +39 -0
- package/dist/esm/resolvers/resolvers.js.map +1 -0
- package/dist/esm/resolvers/utils.d.ts +43 -0
- package/dist/esm/resolvers/utils.js +125 -0
- package/dist/esm/resolvers/utils.js.map +1 -0
- package/dist/esm/utils.d.ts +25 -0
- package/dist/esm/utils.js +159 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/values.d.ts +15 -0
- package/dist/esm/values.js +7 -0
- package/dist/esm/values.js.map +1 -0
- package/jest.config.ts +12 -0
- package/package.json +66 -0
- package/renovate.json +32 -0
- package/src/client/gql.ts +7 -0
- package/src/client/index.ts +4 -0
- package/src/client/queries.ts +251 -0
- package/src/context.ts +27 -0
- package/src/errors.ts +32 -0
- package/src/generate/generate.ts +273 -0
- package/src/generate/index.ts +5 -0
- package/src/generate/mutations.ts +35 -0
- package/src/generate/utils.ts +223 -0
- package/src/index.ts +12 -0
- package/src/migrations/generate.ts +633 -0
- package/src/migrations/index.ts +3 -0
- package/src/models.ts +228 -0
- package/src/permissions/check.ts +239 -0
- package/src/permissions/generate.ts +143 -0
- package/src/permissions/index.ts +4 -0
- package/src/resolvers/arguments.ts +129 -0
- package/src/resolvers/filters.ts +163 -0
- package/src/resolvers/index.ts +9 -0
- package/src/resolvers/mutations.ts +313 -0
- package/src/resolvers/node.ts +193 -0
- package/src/resolvers/resolver.ts +223 -0
- package/src/resolvers/resolvers.ts +40 -0
- package/src/resolvers/utils.ts +188 -0
- package/src/utils.ts +186 -0
- package/src/values.ts +19 -0
- package/tests/unit/__snapshots__/generate.spec.ts.snap +105 -0
- package/tests/unit/__snapshots__/resolve.spec.ts.snap +60 -0
- package/tests/unit/generate.spec.ts +8 -0
- package/tests/unit/resolve.spec.ts +128 -0
- package/tests/unit/utils.ts +82 -0
- package/tsconfig.jest.json +13 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FieldDefinitionNode,
|
|
3
|
+
FieldNode,
|
|
4
|
+
FragmentDefinitionNode,
|
|
5
|
+
InlineFragmentNode,
|
|
6
|
+
ObjectTypeDefinitionNode,
|
|
7
|
+
SelectionNode,
|
|
8
|
+
} from 'graphql';
|
|
9
|
+
|
|
10
|
+
import { FullContext } from '../context';
|
|
11
|
+
import { isJsonObjectModel, Model } from '../models';
|
|
12
|
+
import { get, summonByKey, summonByName } from '../utils';
|
|
13
|
+
import {
|
|
14
|
+
getFragmentTypeName,
|
|
15
|
+
getNameOrAlias,
|
|
16
|
+
getType,
|
|
17
|
+
getTypeName,
|
|
18
|
+
isFieldNode,
|
|
19
|
+
isFragmentSpreadNode,
|
|
20
|
+
isInlineFragmentNode,
|
|
21
|
+
isListType,
|
|
22
|
+
} from './utils';
|
|
23
|
+
|
|
24
|
+
export type ResolverNode = {
|
|
25
|
+
ctx: FullContext;
|
|
26
|
+
|
|
27
|
+
tableName: string;
|
|
28
|
+
tableAlias: string;
|
|
29
|
+
shortTableAlias: string;
|
|
30
|
+
|
|
31
|
+
baseTypeDefinition: ObjectTypeDefinitionNode;
|
|
32
|
+
baseModel?: Model;
|
|
33
|
+
|
|
34
|
+
typeDefinition: ObjectTypeDefinitionNode;
|
|
35
|
+
model: Model;
|
|
36
|
+
|
|
37
|
+
selectionSet: readonly SelectionNode[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type FieldResolverNode = ResolverNode & {
|
|
41
|
+
field: FieldNode;
|
|
42
|
+
fieldDefinition: FieldDefinitionNode;
|
|
43
|
+
foreignKey?: string;
|
|
44
|
+
isList: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type WhereNode = {
|
|
48
|
+
ctx: FullContext;
|
|
49
|
+
tableName: string;
|
|
50
|
+
tableAlias: string;
|
|
51
|
+
shortTableAlias: string;
|
|
52
|
+
model: Model;
|
|
53
|
+
|
|
54
|
+
foreignKey?: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const getResolverNode = ({
|
|
58
|
+
ctx,
|
|
59
|
+
node,
|
|
60
|
+
tableAlias,
|
|
61
|
+
baseTypeDefinition,
|
|
62
|
+
typeName,
|
|
63
|
+
}: {
|
|
64
|
+
ctx: FullContext;
|
|
65
|
+
node: FieldNode | InlineFragmentNode | FragmentDefinitionNode;
|
|
66
|
+
baseTypeDefinition: ObjectTypeDefinitionNode;
|
|
67
|
+
tableAlias: string;
|
|
68
|
+
typeName: string;
|
|
69
|
+
}): ResolverNode => ({
|
|
70
|
+
ctx,
|
|
71
|
+
tableName: typeName,
|
|
72
|
+
tableAlias,
|
|
73
|
+
shortTableAlias: ctx.aliases.getShort(tableAlias),
|
|
74
|
+
baseTypeDefinition,
|
|
75
|
+
baseModel: ctx.models.find((model) => model.name === baseTypeDefinition.name.value),
|
|
76
|
+
typeDefinition: getType(ctx.info.schema, typeName),
|
|
77
|
+
model: summonByName(ctx.models, typeName),
|
|
78
|
+
selectionSet: get(node.selectionSet, 'selections'),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const getRootFieldNode = ({
|
|
82
|
+
ctx,
|
|
83
|
+
node,
|
|
84
|
+
baseTypeDefinition,
|
|
85
|
+
}: {
|
|
86
|
+
ctx: FullContext;
|
|
87
|
+
node: FieldNode;
|
|
88
|
+
baseTypeDefinition: ObjectTypeDefinitionNode;
|
|
89
|
+
}): FieldResolverNode => {
|
|
90
|
+
const fieldName = node.name.value;
|
|
91
|
+
const fieldDefinition = summonByKey(baseTypeDefinition.fields || [], 'name.value', fieldName);
|
|
92
|
+
|
|
93
|
+
const typeName = getTypeName(fieldDefinition.type);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ctx,
|
|
97
|
+
tableName: typeName,
|
|
98
|
+
tableAlias: typeName,
|
|
99
|
+
shortTableAlias: ctx.aliases.getShort(typeName),
|
|
100
|
+
baseTypeDefinition,
|
|
101
|
+
typeDefinition: getType(ctx.info.schema, typeName),
|
|
102
|
+
model: summonByName(ctx.models, typeName),
|
|
103
|
+
selectionSet: get(node.selectionSet, 'selections'),
|
|
104
|
+
field: node,
|
|
105
|
+
fieldDefinition,
|
|
106
|
+
isList: isListType(fieldDefinition.type),
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const getSimpleFields = (node: ResolverNode) => {
|
|
111
|
+
return node.selectionSet.filter(isFieldNode).filter((selection) => {
|
|
112
|
+
if (!selection.selectionSet) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return node.model.fields.some(({ json, name }) => json && name === selection.name.value);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const getInlineFragments = (node: ResolverNode) =>
|
|
121
|
+
node.selectionSet.filter(isInlineFragmentNode).map((subNode) =>
|
|
122
|
+
getResolverNode({
|
|
123
|
+
ctx: node.ctx,
|
|
124
|
+
node: subNode,
|
|
125
|
+
tableAlias: node.tableAlias + '__' + getFragmentTypeName(subNode),
|
|
126
|
+
baseTypeDefinition: node.baseTypeDefinition,
|
|
127
|
+
typeName: getFragmentTypeName(subNode),
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
export const getFragmentSpreads = (node: ResolverNode) =>
|
|
132
|
+
node.selectionSet.filter(isFragmentSpreadNode).map((subNode) =>
|
|
133
|
+
getResolverNode({
|
|
134
|
+
ctx: node.ctx,
|
|
135
|
+
node: node.ctx.info.fragments[subNode.name.value]!,
|
|
136
|
+
tableAlias: node.tableAlias,
|
|
137
|
+
baseTypeDefinition: node.baseTypeDefinition,
|
|
138
|
+
typeName: node.model.name,
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
export const getJoins = (node: ResolverNode, toMany: boolean) => {
|
|
143
|
+
const nodes: FieldResolverNode[] = [];
|
|
144
|
+
for (const subNode of node.selectionSet.filter(isFieldNode).filter(({ selectionSet }) => selectionSet)) {
|
|
145
|
+
const ctx = node.ctx;
|
|
146
|
+
const baseTypeDefinition = node.typeDefinition;
|
|
147
|
+
const fieldName = subNode.name.value;
|
|
148
|
+
const fieldNameOrAlias = getNameOrAlias(subNode);
|
|
149
|
+
const fieldDefinition = summonByKey(baseTypeDefinition.fields || [], 'name.value', fieldName);
|
|
150
|
+
|
|
151
|
+
const typeName = getTypeName(fieldDefinition.type);
|
|
152
|
+
|
|
153
|
+
if (isJsonObjectModel(summonByName(ctx.rawModels, typeName))) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const baseModel = summonByName(ctx.models, baseTypeDefinition.name.value);
|
|
158
|
+
|
|
159
|
+
let foreignKey;
|
|
160
|
+
if (toMany) {
|
|
161
|
+
const reverseRelation = baseModel.reverseRelationsByName[fieldName];
|
|
162
|
+
if (!reverseRelation) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
foreignKey = reverseRelation.foreignKey;
|
|
166
|
+
} else {
|
|
167
|
+
const modelField = baseModel.fieldsByName[fieldName];
|
|
168
|
+
if (!modelField || modelField.raw) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
foreignKey = modelField.foreignKey;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const tableAlias = node.tableAlias + '__' + fieldNameOrAlias;
|
|
175
|
+
|
|
176
|
+
nodes.push({
|
|
177
|
+
ctx,
|
|
178
|
+
tableName: typeName,
|
|
179
|
+
tableAlias,
|
|
180
|
+
shortTableAlias: ctx.aliases.getShort(tableAlias),
|
|
181
|
+
baseTypeDefinition,
|
|
182
|
+
baseModel,
|
|
183
|
+
typeDefinition: getType(ctx.info.schema, typeName),
|
|
184
|
+
model: summonByName(ctx.models, typeName),
|
|
185
|
+
selectionSet: get(subNode.selectionSet, 'selections'),
|
|
186
|
+
field: subNode,
|
|
187
|
+
fieldDefinition,
|
|
188
|
+
foreignKey,
|
|
189
|
+
isList: isListType(fieldDefinition.type),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return nodes;
|
|
193
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { GraphQLResolveInfo } from 'graphql';
|
|
2
|
+
import { Knex } from 'knex';
|
|
3
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
4
|
+
import flatMap from 'lodash/flatMap';
|
|
5
|
+
import { Context, FullContext } from '../context';
|
|
6
|
+
import { NotFoundError, PermissionError } from '../errors';
|
|
7
|
+
import { applyPermissions } from '../permissions/check';
|
|
8
|
+
import { PermissionStack } from '../permissions/generate';
|
|
9
|
+
import { get, summonByKey } from '../utils';
|
|
10
|
+
import { applyFilters } from './filters';
|
|
11
|
+
import {
|
|
12
|
+
FieldResolverNode,
|
|
13
|
+
ResolverNode,
|
|
14
|
+
getFragmentSpreads,
|
|
15
|
+
getInlineFragments,
|
|
16
|
+
getJoins,
|
|
17
|
+
getRootFieldNode,
|
|
18
|
+
getSimpleFields,
|
|
19
|
+
} from './node';
|
|
20
|
+
import { AliasGenerator, Entry, ID_ALIAS, Joins, addJoin, applyJoins, getNameOrAlias, hydrate, isListType } from './utils';
|
|
21
|
+
|
|
22
|
+
export const queryResolver = (_parent: any, _args: any, ctx: Context, info: GraphQLResolveInfo) =>
|
|
23
|
+
resolve({ ...ctx, info, aliases: new AliasGenerator() });
|
|
24
|
+
|
|
25
|
+
export const resolve = async (ctx: FullContext, id?: string) => {
|
|
26
|
+
const fieldNode = summonByKey(ctx.info.fieldNodes, 'name.value', ctx.info.fieldName);
|
|
27
|
+
const baseTypeDefinition = get(
|
|
28
|
+
ctx.info.operation.operation === 'query' ? ctx.info.schema.getQueryType() : ctx.info.schema.getMutationType(),
|
|
29
|
+
'astNode'
|
|
30
|
+
);
|
|
31
|
+
const node = getRootFieldNode({
|
|
32
|
+
ctx,
|
|
33
|
+
node: fieldNode,
|
|
34
|
+
baseTypeDefinition,
|
|
35
|
+
});
|
|
36
|
+
const { query, verifiedPermissionStacks } = await buildQuery(node);
|
|
37
|
+
|
|
38
|
+
if (ctx.info.fieldName === 'me') {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
40
|
+
query.where({ [`${node.shortTableAlias}.id`]: node.ctx.user.id });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!node.isList) {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
45
|
+
query.limit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (id) {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
50
|
+
query.where({ id });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const raw = await query;
|
|
54
|
+
|
|
55
|
+
const res = hydrate(node, raw);
|
|
56
|
+
|
|
57
|
+
await applySubQueries(node, res, verifiedPermissionStacks);
|
|
58
|
+
|
|
59
|
+
if (node.isList) {
|
|
60
|
+
return res;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!res[0]) {
|
|
64
|
+
throw new NotFoundError('Entity not found');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return res[0];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type VerifiedPermissionStacks = Record<string, PermissionStack>;
|
|
71
|
+
|
|
72
|
+
const buildQuery = async (
|
|
73
|
+
node: FieldResolverNode,
|
|
74
|
+
parentVerifiedPermissionStacks?: VerifiedPermissionStacks
|
|
75
|
+
): Promise<{ query: Knex.QueryBuilder; verifiedPermissionStacks: VerifiedPermissionStacks }> => {
|
|
76
|
+
const { tableAlias, shortTableAlias, tableName, model, ctx } = node;
|
|
77
|
+
const query = ctx.knex.fromRaw(`"${tableName}" as "${shortTableAlias}"`);
|
|
78
|
+
|
|
79
|
+
const joins: Joins = {};
|
|
80
|
+
applyFilters(node, query, joins);
|
|
81
|
+
applySelects(node, query, joins);
|
|
82
|
+
applyJoins(node.ctx.aliases, query, joins);
|
|
83
|
+
|
|
84
|
+
const tables = [
|
|
85
|
+
[model.name, tableAlias] as const,
|
|
86
|
+
...Object.keys(joins).map((tableName) => tableName.split(':') as [string, string]),
|
|
87
|
+
];
|
|
88
|
+
const verifiedPermissionStacks: VerifiedPermissionStacks = {};
|
|
89
|
+
for (const [table, alias] of tables) {
|
|
90
|
+
const verifiedPermissionStack = applyPermissions(
|
|
91
|
+
ctx,
|
|
92
|
+
table,
|
|
93
|
+
node.ctx.aliases.getShort(alias),
|
|
94
|
+
query,
|
|
95
|
+
'READ',
|
|
96
|
+
parentVerifiedPermissionStacks?.[alias.split('__').slice(0, -1).join('__')]
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (typeof verifiedPermissionStack !== 'boolean') {
|
|
100
|
+
verifiedPermissionStacks[alias] = verifiedPermissionStack;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { query, verifiedPermissionStacks };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const applySelects = (node: ResolverNode, query: Knex.QueryBuilder, joins: Joins) => {
|
|
108
|
+
// Simple field selects
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
110
|
+
query.select(
|
|
111
|
+
...[
|
|
112
|
+
{ field: 'id', alias: ID_ALIAS },
|
|
113
|
+
...getSimpleFields(node)
|
|
114
|
+
.filter((n) => {
|
|
115
|
+
const field = node.model.fields.find(({ name }) => name === n.name.value);
|
|
116
|
+
|
|
117
|
+
if (!field || field.relation || field.raw) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (field.queriableBy && !field.queriableBy.includes(node.ctx.user.role)) {
|
|
122
|
+
throw new PermissionError('READ', `${node.model.name}'s field "${field.name}"`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
})
|
|
127
|
+
.map((n) => ({ field: n.name.value, alias: getNameOrAlias(n) })),
|
|
128
|
+
].map(
|
|
129
|
+
({ field, alias }: { field: string; alias: string }) =>
|
|
130
|
+
`${node.shortTableAlias}.${field} as ${node.shortTableAlias}__${alias}`
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
for (const subNode of getInlineFragments(node)) {
|
|
135
|
+
applySelects(subNode, query, joins);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const subNode of getFragmentSpreads(node)) {
|
|
139
|
+
applySelects(subNode, query, joins);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const subNode of getJoins(node, false)) {
|
|
143
|
+
addJoin(joins, node.tableAlias, subNode.tableName, subNode.tableAlias, get(subNode, 'foreignKey'), 'id');
|
|
144
|
+
|
|
145
|
+
applySelects(subNode, query, joins);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const applySubQueries = async (
|
|
150
|
+
node: ResolverNode,
|
|
151
|
+
entries: Entry[],
|
|
152
|
+
parentVerifiedPermissionStacks: VerifiedPermissionStacks
|
|
153
|
+
): Promise<void> => {
|
|
154
|
+
if (!entries.length) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const entriesById: { [id: string]: Entry[] } = {};
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (!entriesById[entry[ID_ALIAS]]) {
|
|
161
|
+
entriesById[entry[ID_ALIAS]] = [];
|
|
162
|
+
}
|
|
163
|
+
entriesById[entry[ID_ALIAS]!]!.push(entry);
|
|
164
|
+
}
|
|
165
|
+
const ids = Object.keys(entriesById);
|
|
166
|
+
|
|
167
|
+
// One to many
|
|
168
|
+
for (const subNode of getJoins(node, true)) {
|
|
169
|
+
const fieldName = getNameOrAlias(subNode.field);
|
|
170
|
+
const isList = isListType(subNode.fieldDefinition.type);
|
|
171
|
+
entries.forEach((entry) => (entry[fieldName] = isList ? [] : null));
|
|
172
|
+
const foreignKey = get(subNode, 'foreignKey');
|
|
173
|
+
const { query, verifiedPermissionStacks } = await buildQuery(subNode, parentVerifiedPermissionStacks);
|
|
174
|
+
const queries = ids.map((id) =>
|
|
175
|
+
query
|
|
176
|
+
.clone()
|
|
177
|
+
.select(`${subNode.shortTableAlias}.${foreignKey} as ${subNode.shortTableAlias}__${foreignKey}`)
|
|
178
|
+
.where({ [`${subNode.shortTableAlias}.${foreignKey}`]: id })
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// TODO: make unionAll faster then promise.all...
|
|
182
|
+
// const rawChildren = await node.ctx.knex.queryBuilder().unionAll(queries, true);
|
|
183
|
+
const rawChildren = (await Promise.all(queries)).flat();
|
|
184
|
+
const children = hydrate(subNode, rawChildren);
|
|
185
|
+
|
|
186
|
+
for (const child of children) {
|
|
187
|
+
for (const entry of entriesById[child[foreignKey] as string]!) {
|
|
188
|
+
if (isList) {
|
|
189
|
+
(entry[fieldName] as Entry[]).push(cloneDeep(child));
|
|
190
|
+
} else {
|
|
191
|
+
entry[fieldName] = cloneDeep(child);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await applySubQueries(
|
|
197
|
+
subNode,
|
|
198
|
+
flatMap(
|
|
199
|
+
entries.map((entry) => {
|
|
200
|
+
const children = entry[fieldName];
|
|
201
|
+
return (isList ? children : children ? [children] : []) as Entry[];
|
|
202
|
+
})
|
|
203
|
+
),
|
|
204
|
+
verifiedPermissionStacks
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const subNode of getInlineFragments(node)) {
|
|
209
|
+
await applySubQueries(subNode, entries, parentVerifiedPermissionStacks);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const subNode of getFragmentSpreads(node)) {
|
|
213
|
+
await applySubQueries(subNode, entries, parentVerifiedPermissionStacks);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const subNode of getJoins(node, false)) {
|
|
217
|
+
await applySubQueries(
|
|
218
|
+
subNode,
|
|
219
|
+
entries.map((item) => item[getNameOrAlias(subNode.field)] as Entry).filter(Boolean),
|
|
220
|
+
parentVerifiedPermissionStacks
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Models } from '../models';
|
|
2
|
+
import { getModelPluralField, merge, typeToField } from '../utils';
|
|
3
|
+
import { mutationResolver } from './mutations';
|
|
4
|
+
import { queryResolver } from './resolver';
|
|
5
|
+
|
|
6
|
+
export const getResolvers = (models: Models) => ({
|
|
7
|
+
Query: merge([
|
|
8
|
+
{
|
|
9
|
+
me: queryResolver,
|
|
10
|
+
},
|
|
11
|
+
...models
|
|
12
|
+
.filter(({ queriable }) => queriable)
|
|
13
|
+
.map((model) => ({
|
|
14
|
+
[typeToField(model.name)]: queryResolver,
|
|
15
|
+
})),
|
|
16
|
+
...models
|
|
17
|
+
.filter(({ listQueriable }) => listQueriable)
|
|
18
|
+
.map((model) => ({
|
|
19
|
+
[getModelPluralField(model)]: queryResolver,
|
|
20
|
+
})),
|
|
21
|
+
]),
|
|
22
|
+
Mutation: merge<unknown>([
|
|
23
|
+
...models
|
|
24
|
+
.filter(({ creatable }) => creatable)
|
|
25
|
+
.map((model) => ({
|
|
26
|
+
[`create${model.name}`]: mutationResolver,
|
|
27
|
+
})),
|
|
28
|
+
...models
|
|
29
|
+
.filter(({ updatable }) => updatable)
|
|
30
|
+
.map((model) => ({
|
|
31
|
+
[`update${model.name}`]: mutationResolver,
|
|
32
|
+
})),
|
|
33
|
+
...models
|
|
34
|
+
.filter(({ deletable }) => deletable)
|
|
35
|
+
.map((model) => ({
|
|
36
|
+
[`delete${model.name}`]: mutationResolver,
|
|
37
|
+
[`restore${model.name}`]: mutationResolver,
|
|
38
|
+
})),
|
|
39
|
+
]),
|
|
40
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import type {
|
|
3
|
+
ASTNode,
|
|
4
|
+
FieldNode,
|
|
5
|
+
FragmentDefinitionNode,
|
|
6
|
+
FragmentSpreadNode,
|
|
7
|
+
GraphQLObjectType,
|
|
8
|
+
GraphQLSchema,
|
|
9
|
+
InlineFragmentNode,
|
|
10
|
+
TypeNode,
|
|
11
|
+
} from 'graphql';
|
|
12
|
+
import { Kind } from 'graphql';
|
|
13
|
+
import { Knex } from 'knex';
|
|
14
|
+
import { UserInputError } from '../errors';
|
|
15
|
+
import { get, it } from '../utils';
|
|
16
|
+
import { Value } from '../values';
|
|
17
|
+
import { FieldResolverNode } from './node';
|
|
18
|
+
|
|
19
|
+
export const ID_ALIAS = 'ID';
|
|
20
|
+
|
|
21
|
+
export type VariableValues = {
|
|
22
|
+
[variableName: string]: Value;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getTypeName = (t: TypeNode): string => {
|
|
26
|
+
switch (t.kind) {
|
|
27
|
+
case 'ListType':
|
|
28
|
+
case 'NonNullType':
|
|
29
|
+
return getTypeName(t.type);
|
|
30
|
+
default:
|
|
31
|
+
return t.name.value;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const isListType = (type: TypeNode): boolean => {
|
|
36
|
+
switch (type.kind) {
|
|
37
|
+
case 'ListType':
|
|
38
|
+
return true;
|
|
39
|
+
case 'NonNullType':
|
|
40
|
+
return isListType(type.type);
|
|
41
|
+
default:
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const isFieldNode = (n: ASTNode): n is FieldNode => n.kind === Kind.FIELD;
|
|
47
|
+
|
|
48
|
+
export const isInlineFragmentNode = (n: ASTNode): n is InlineFragmentNode => n.kind === Kind.INLINE_FRAGMENT;
|
|
49
|
+
|
|
50
|
+
export const isFragmentSpreadNode = (n: ASTNode): n is FragmentSpreadNode => n.kind === Kind.FRAGMENT_SPREAD;
|
|
51
|
+
|
|
52
|
+
export type Maybe<T> = null | undefined | T;
|
|
53
|
+
|
|
54
|
+
export const getType = (schema: GraphQLSchema, name: string) =>
|
|
55
|
+
get(schema.getType(name) as Maybe<GraphQLObjectType>, 'astNode');
|
|
56
|
+
|
|
57
|
+
export const getFragmentTypeName = (node: InlineFragmentNode | FragmentDefinitionNode) =>
|
|
58
|
+
get(get(node.typeCondition, 'name'), 'value');
|
|
59
|
+
|
|
60
|
+
export type Entry = {
|
|
61
|
+
[ID_ALIAS]: string;
|
|
62
|
+
[field: string]: null | string | number | Entry | Entry[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function hydrate<T extends Entry>(
|
|
66
|
+
node: FieldResolverNode,
|
|
67
|
+
raw: { [key: string]: undefined | null | string | Date | number }[]
|
|
68
|
+
): T[] {
|
|
69
|
+
const tableAlias = node.tableAlias;
|
|
70
|
+
const res = raw.map((entry) => {
|
|
71
|
+
const res: any = {};
|
|
72
|
+
outer: for (const [column, value] of Object.entries(entry)) {
|
|
73
|
+
let current = res;
|
|
74
|
+
const shortParts = column.split('__');
|
|
75
|
+
const fieldName = shortParts.pop()!;
|
|
76
|
+
const columnWithoutField = shortParts.join('__');
|
|
77
|
+
const longColumn = node.ctx.aliases.getLong(columnWithoutField);
|
|
78
|
+
const longColumnWithoutRoot = longColumn.replace(new RegExp(`^${tableAlias}(__)?`), '');
|
|
79
|
+
const allParts = [tableAlias, ...(longColumnWithoutRoot ? longColumnWithoutRoot.split('__') : []), fieldName];
|
|
80
|
+
for (let i = 0; i < allParts.length - 1; i++) {
|
|
81
|
+
const part = allParts[i]!;
|
|
82
|
+
|
|
83
|
+
if (!current[part]) {
|
|
84
|
+
const idField = [node.ctx.aliases.getShort(allParts.slice(0, i + 1).join('__')), ID_ALIAS].join('__');
|
|
85
|
+
if (!entry[idField]) {
|
|
86
|
+
continue outer;
|
|
87
|
+
}
|
|
88
|
+
current[part] = {};
|
|
89
|
+
}
|
|
90
|
+
current = current[part];
|
|
91
|
+
}
|
|
92
|
+
current[it(fieldName)] = value;
|
|
93
|
+
}
|
|
94
|
+
return res[tableAlias];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return res;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const ors = (query: Knex.QueryBuilder, [first, ...rest]: ((query: Knex.QueryBuilder) => Knex.QueryBuilder)[]) => {
|
|
101
|
+
if (!first) {
|
|
102
|
+
return query;
|
|
103
|
+
}
|
|
104
|
+
return query.where((subQuery) => {
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
106
|
+
subQuery.where((subSubQuery) => {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
108
|
+
first(subSubQuery);
|
|
109
|
+
});
|
|
110
|
+
for (const cb of rest) {
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
112
|
+
subQuery.orWhere((subSubQuery) => {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
114
|
+
cb(subSubQuery);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const getNameOrAlias = (node: FieldNode) => {
|
|
121
|
+
const name = node.alias ? node.alias.value : node.name.value;
|
|
122
|
+
|
|
123
|
+
if ([ID_ALIAS].indexOf(name) >= 0) {
|
|
124
|
+
throw new UserInputError(`"${name}" can not be used as alias since it's a reserved word`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return name;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type Ops<T> = ((target: T) => T)[];
|
|
131
|
+
|
|
132
|
+
export const apply = <T>(target: T, ops: ((target: T) => T)[]) => ops.reduce((target, op) => op(target), target);
|
|
133
|
+
|
|
134
|
+
export type Joins = Record<`${string}:${string}`, { table1Alias: string; column1: string; column2: string }>;
|
|
135
|
+
|
|
136
|
+
export const applyJoins = (aliases: AliasGenerator, query: Knex.QueryBuilder, joins: Joins) => {
|
|
137
|
+
for (const [tableName, { table1Alias, column1, column2 }] of Object.entries(joins)) {
|
|
138
|
+
const [table, alias] = tableName.split(':');
|
|
139
|
+
const table1ShortAlias = aliases.getShort(table1Alias);
|
|
140
|
+
const table2ShortAlias = aliases.getShort(alias);
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
|
|
142
|
+
query.leftJoin(`${table} as ${table2ShortAlias}`, `${table1ShortAlias}.${column1}`, `${table2ShortAlias}.${column2}`);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Note: modifies the join parameter.
|
|
148
|
+
*/
|
|
149
|
+
export const addJoin = (
|
|
150
|
+
joins: Joins,
|
|
151
|
+
table1Alias: string,
|
|
152
|
+
table2: string,
|
|
153
|
+
alias2: string,
|
|
154
|
+
column1: string,
|
|
155
|
+
column2: string
|
|
156
|
+
) => {
|
|
157
|
+
joins[`${table2}:${alias2}`] ||= { table1Alias, column1, column2 };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export class AliasGenerator {
|
|
161
|
+
private reverse: Record<string, string> = {};
|
|
162
|
+
|
|
163
|
+
public getShort(long?: string) {
|
|
164
|
+
if (!long) {
|
|
165
|
+
const short = `a${Object.keys(this.reverse).length}`;
|
|
166
|
+
return (this.reverse[short] = short);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const shortPrefix = long
|
|
170
|
+
.split('__')
|
|
171
|
+
.map((part) => part[0] + part.slice(1).replaceAll(/[a-z]+/g, ''))
|
|
172
|
+
.join('__');
|
|
173
|
+
let postfix = 0;
|
|
174
|
+
let short = shortPrefix + (postfix || '');
|
|
175
|
+
while (short in this.reverse && this.reverse[short] !== long) {
|
|
176
|
+
short = shortPrefix + ++postfix;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.reverse[short] = long;
|
|
180
|
+
return short;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public getLong(short: string) {
|
|
184
|
+
return get(this.reverse, short);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const hash = (s: any) => createHash('md5').update(JSON.stringify(s)).digest('hex');
|