@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.
Files changed (124) hide show
  1. package/.eslintrc +21 -0
  2. package/.github/workflows/release.yml +24 -0
  3. package/.github/workflows/testing.yml +37 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +34 -0
  6. package/.prettierrc.json +1 -0
  7. package/.releaserc +27 -0
  8. package/CHANGELOG.md +6 -0
  9. package/README.md +15 -0
  10. package/dist/cjs/index.cjs +2646 -0
  11. package/dist/esm/client/gql.d.ts +1 -0
  12. package/dist/esm/client/gql.js +5 -0
  13. package/dist/esm/client/gql.js.map +1 -0
  14. package/dist/esm/client/index.d.ts +2 -0
  15. package/dist/esm/client/index.js +4 -0
  16. package/dist/esm/client/index.js.map +1 -0
  17. package/dist/esm/client/queries.d.ts +24 -0
  18. package/dist/esm/client/queries.js +152 -0
  19. package/dist/esm/client/queries.js.map +1 -0
  20. package/dist/esm/context.d.ts +30 -0
  21. package/dist/esm/context.js +2 -0
  22. package/dist/esm/context.js.map +1 -0
  23. package/dist/esm/errors.d.ts +17 -0
  24. package/dist/esm/errors.js +27 -0
  25. package/dist/esm/errors.js.map +1 -0
  26. package/dist/esm/generate/generate.d.ts +7 -0
  27. package/dist/esm/generate/generate.js +211 -0
  28. package/dist/esm/generate/generate.js.map +1 -0
  29. package/dist/esm/generate/index.d.ts +3 -0
  30. package/dist/esm/generate/index.js +5 -0
  31. package/dist/esm/generate/index.js.map +1 -0
  32. package/dist/esm/generate/mutations.d.ts +2 -0
  33. package/dist/esm/generate/mutations.js +18 -0
  34. package/dist/esm/generate/mutations.js.map +1 -0
  35. package/dist/esm/generate/utils.d.ts +22 -0
  36. package/dist/esm/generate/utils.js +150 -0
  37. package/dist/esm/generate/utils.js.map +1 -0
  38. package/dist/esm/index.d.ts +10 -0
  39. package/dist/esm/index.js +12 -0
  40. package/dist/esm/index.js.map +1 -0
  41. package/dist/esm/migrations/generate.d.ts +28 -0
  42. package/dist/esm/migrations/generate.js +516 -0
  43. package/dist/esm/migrations/generate.js.map +1 -0
  44. package/dist/esm/migrations/index.d.ts +1 -0
  45. package/dist/esm/migrations/index.js +3 -0
  46. package/dist/esm/migrations/index.js.map +1 -0
  47. package/dist/esm/models.d.ts +170 -0
  48. package/dist/esm/models.js +27 -0
  49. package/dist/esm/models.js.map +1 -0
  50. package/dist/esm/permissions/check.d.ts +15 -0
  51. package/dist/esm/permissions/check.js +162 -0
  52. package/dist/esm/permissions/check.js.map +1 -0
  53. package/dist/esm/permissions/generate.d.ts +45 -0
  54. package/dist/esm/permissions/generate.js +77 -0
  55. package/dist/esm/permissions/generate.js.map +1 -0
  56. package/dist/esm/permissions/index.d.ts +2 -0
  57. package/dist/esm/permissions/index.js +4 -0
  58. package/dist/esm/permissions/index.js.map +1 -0
  59. package/dist/esm/resolvers/arguments.d.ts +26 -0
  60. package/dist/esm/resolvers/arguments.js +88 -0
  61. package/dist/esm/resolvers/arguments.js.map +1 -0
  62. package/dist/esm/resolvers/filters.d.ts +5 -0
  63. package/dist/esm/resolvers/filters.js +126 -0
  64. package/dist/esm/resolvers/filters.js.map +1 -0
  65. package/dist/esm/resolvers/index.d.ts +7 -0
  66. package/dist/esm/resolvers/index.js +9 -0
  67. package/dist/esm/resolvers/index.js.map +1 -0
  68. package/dist/esm/resolvers/mutations.d.ts +3 -0
  69. package/dist/esm/resolvers/mutations.js +255 -0
  70. package/dist/esm/resolvers/mutations.js.map +1 -0
  71. package/dist/esm/resolvers/node.d.ts +44 -0
  72. package/dist/esm/resolvers/node.js +102 -0
  73. package/dist/esm/resolvers/node.js.map +1 -0
  74. package/dist/esm/resolvers/resolver.d.ts +5 -0
  75. package/dist/esm/resolvers/resolver.js +143 -0
  76. package/dist/esm/resolvers/resolver.js.map +1 -0
  77. package/dist/esm/resolvers/resolvers.d.ts +9 -0
  78. package/dist/esm/resolvers/resolvers.js +39 -0
  79. package/dist/esm/resolvers/resolvers.js.map +1 -0
  80. package/dist/esm/resolvers/utils.d.ts +43 -0
  81. package/dist/esm/resolvers/utils.js +125 -0
  82. package/dist/esm/resolvers/utils.js.map +1 -0
  83. package/dist/esm/utils.d.ts +25 -0
  84. package/dist/esm/utils.js +159 -0
  85. package/dist/esm/utils.js.map +1 -0
  86. package/dist/esm/values.d.ts +15 -0
  87. package/dist/esm/values.js +7 -0
  88. package/dist/esm/values.js.map +1 -0
  89. package/jest.config.ts +12 -0
  90. package/package.json +66 -0
  91. package/renovate.json +32 -0
  92. package/src/client/gql.ts +7 -0
  93. package/src/client/index.ts +4 -0
  94. package/src/client/queries.ts +251 -0
  95. package/src/context.ts +27 -0
  96. package/src/errors.ts +32 -0
  97. package/src/generate/generate.ts +273 -0
  98. package/src/generate/index.ts +5 -0
  99. package/src/generate/mutations.ts +35 -0
  100. package/src/generate/utils.ts +223 -0
  101. package/src/index.ts +12 -0
  102. package/src/migrations/generate.ts +633 -0
  103. package/src/migrations/index.ts +3 -0
  104. package/src/models.ts +228 -0
  105. package/src/permissions/check.ts +239 -0
  106. package/src/permissions/generate.ts +143 -0
  107. package/src/permissions/index.ts +4 -0
  108. package/src/resolvers/arguments.ts +129 -0
  109. package/src/resolvers/filters.ts +163 -0
  110. package/src/resolvers/index.ts +9 -0
  111. package/src/resolvers/mutations.ts +313 -0
  112. package/src/resolvers/node.ts +193 -0
  113. package/src/resolvers/resolver.ts +223 -0
  114. package/src/resolvers/resolvers.ts +40 -0
  115. package/src/resolvers/utils.ts +188 -0
  116. package/src/utils.ts +186 -0
  117. package/src/values.ts +19 -0
  118. package/tests/unit/__snapshots__/generate.spec.ts.snap +105 -0
  119. package/tests/unit/__snapshots__/resolve.spec.ts.snap +60 -0
  120. package/tests/unit/generate.spec.ts +8 -0
  121. package/tests/unit/resolve.spec.ts +128 -0
  122. package/tests/unit/utils.ts +82 -0
  123. package/tsconfig.jest.json +13 -0
  124. package/tsconfig.json +13 -0
@@ -0,0 +1,129 @@
1
+ import type { GraphQLObjectType, GraphQLSchema, TypeDefinitionNode, TypeNode, ValueNode } from 'graphql';
2
+ import { Kind } from 'graphql';
3
+ import { summonByKey } from '../utils';
4
+ import { Value } from '../values';
5
+ import { FieldResolverNode } from './node';
6
+ import { Maybe, VariableValues } from './utils';
7
+
8
+ export type Where = Record<string, Value>;
9
+
10
+ export type OrderBy = Record<string, 'ASC' | 'DESC'>[];
11
+
12
+ export type Args = {
13
+ where?: Where | null;
14
+ };
15
+
16
+ export type ListArgs = {
17
+ limit?: number | null | undefined;
18
+ offset?: number | null | undefined;
19
+ orderBy?: string[];
20
+ };
21
+
22
+ export type NormalizedArguments = {
23
+ limit?: number;
24
+ offset?: number;
25
+ orderBy?: OrderBy;
26
+ where?: Where;
27
+ search?: string;
28
+ mine?: boolean;
29
+ language?: string;
30
+ };
31
+
32
+ function getRawValue(value: ValueNode, values?: VariableValues): Value {
33
+ switch (value.kind) {
34
+ case Kind.LIST:
35
+ if (!values) {
36
+ return;
37
+ }
38
+ return value.values.map((value) => getRawValue(value, values));
39
+ case Kind.VARIABLE:
40
+ return values?.[value.name.value];
41
+ case Kind.INT:
42
+ return parseInt(value.value, 10);
43
+ case Kind.NULL:
44
+ return null;
45
+ case Kind.FLOAT:
46
+ return parseFloat(value.value);
47
+ case Kind.STRING:
48
+ case Kind.BOOLEAN:
49
+ case Kind.ENUM:
50
+ return value.value;
51
+ case Kind.OBJECT: {
52
+ if (!value.fields.length) {
53
+ return;
54
+ }
55
+ const res: Record<string, Value> = {};
56
+ for (const field of value.fields) {
57
+ res[field.name.value] = getRawValue(field.value, values);
58
+ }
59
+ return res;
60
+ }
61
+ }
62
+ }
63
+
64
+ export const normalizeArguments = (node: FieldResolverNode) => {
65
+ const normalizedArguments: NormalizedArguments = {};
66
+ if (node.field.arguments) {
67
+ for (const argument of node.field.arguments) {
68
+ const rawValue = getRawValue(argument.value, node.ctx.info.variableValues);
69
+ const normalizedValue = normalizeValue(
70
+ rawValue,
71
+ summonByKey(node.fieldDefinition.arguments || [], 'name.value', argument.name.value).type,
72
+ node.ctx.info.schema
73
+ );
74
+ if (normalizedValue === undefined) {
75
+ continue;
76
+ }
77
+ normalizedArguments[argument.name.value] = normalizedValue as any;
78
+ }
79
+ }
80
+ return normalizedArguments;
81
+ };
82
+
83
+ export function normalizeValue(value: Value, type: TypeNode, schema: GraphQLSchema): Value {
84
+ switch (type.kind) {
85
+ case Kind.LIST_TYPE: {
86
+ if (Array.isArray(value)) {
87
+ const res: Value[] = [];
88
+ for (const v of value) {
89
+ res.push(normalizeValue(v, type.type, schema));
90
+ }
91
+ return res;
92
+ }
93
+
94
+ const normalizedValue = normalizeValue(value, type.type, schema);
95
+ if (normalizedValue === undefined) {
96
+ return;
97
+ }
98
+
99
+ return [normalizedValue];
100
+ }
101
+ case Kind.NON_NULL_TYPE:
102
+ return normalizeValue(value, type.type, schema);
103
+ case Kind.NAMED_TYPE:
104
+ return normalizeValueByTypeDefinition(
105
+ value,
106
+ (schema.getType(type.name.value) as Maybe<GraphQLObjectType>)?.astNode,
107
+ schema
108
+ );
109
+ }
110
+ }
111
+
112
+ export const normalizeValueByTypeDefinition = (value: Value, type: Maybe<TypeDefinitionNode>, schema: GraphQLSchema) => {
113
+ if (!type || type.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION) {
114
+ return value;
115
+ }
116
+ if (!value) {
117
+ return;
118
+ }
119
+ const res: Record<string, Value> = {};
120
+ for (const key of Object.keys(value)) {
121
+ const field = summonByKey(type.fields, 'name.value', key);
122
+ const normalizedValue = normalizeValue((value as Record<string, Value>)[key], field.type, schema);
123
+ if (normalizedValue === undefined) {
124
+ continue;
125
+ }
126
+ res[key] = normalizedValue;
127
+ }
128
+ return res;
129
+ };
@@ -0,0 +1,163 @@
1
+ import { Knex } from 'knex';
2
+ import { ForbiddenError, UserInputError } from '../errors';
3
+ import { get, summonByName } from '../utils';
4
+ import { OrderBy, Where, normalizeArguments } from './arguments';
5
+ import { FieldResolverNode, WhereNode } from './node';
6
+ import { Joins, Ops, addJoin, apply, ors } from './utils';
7
+
8
+ export const SPECIAL_FILTERS: Record<string, string> = {
9
+ GT: '?? > ?',
10
+ GTE: '?? >= ?',
11
+ LT: '?? < ?',
12
+ LTE: '?? <= ?',
13
+ };
14
+
15
+ export const applyFilters = (node: FieldResolverNode, query: Knex.QueryBuilder, joins: Joins) => {
16
+ const normalizedArguments = normalizeArguments(node);
17
+ if (!normalizedArguments.orderBy) {
18
+ if (node.model.defaultOrderBy) {
19
+ normalizedArguments.orderBy = node.model.defaultOrderBy;
20
+ } else if (node.model.creatable) {
21
+ normalizedArguments.orderBy = [{ createdAt: 'DESC' }];
22
+ }
23
+ }
24
+ if (node.model.deletable) {
25
+ if (!normalizedArguments.where) {
26
+ normalizedArguments.where = {};
27
+ }
28
+ if (
29
+ normalizedArguments.where.deleted &&
30
+ (!Array.isArray(normalizedArguments.where.deleted) || normalizedArguments.where.deleted.some((v) => v))
31
+ ) {
32
+ if (node.ctx.user.role !== 'ADMIN') {
33
+ throw new ForbiddenError('You cannot access deleted entries.');
34
+ }
35
+ } else {
36
+ normalizedArguments.where.deleted = false;
37
+ }
38
+ }
39
+ const { limit, offset, orderBy, where, search } = normalizedArguments;
40
+
41
+ if (limit) {
42
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
43
+ query.limit(limit);
44
+ }
45
+
46
+ if (offset) {
47
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
48
+ query.offset(offset);
49
+ }
50
+
51
+ if (orderBy) {
52
+ applyOrderBy(node, orderBy, query);
53
+ }
54
+
55
+ if (where) {
56
+ const ops: Ops<Knex.QueryBuilder> = [];
57
+ applyWhere(node, where, ops, joins);
58
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
59
+ apply(query, ops);
60
+ }
61
+
62
+ if (search) {
63
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
64
+ applySearch(node, search, query);
65
+ }
66
+ };
67
+
68
+ const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>, joins: Joins) => {
69
+ for (const key of Object.keys(where)) {
70
+ const value = where[key];
71
+
72
+ const specialFilter = key.match(/^(\w+)_(\w+)$/);
73
+ if (specialFilter) {
74
+ const [, actualKey, filter] = specialFilter;
75
+ if (!SPECIAL_FILTERS[filter!]) {
76
+ // Should not happen
77
+ throw new Error(`Invalid filter ${key}.`);
78
+ }
79
+ ops.push((query) =>
80
+ query.whereRaw(SPECIAL_FILTERS[filter!]!, [`${node.shortTableAlias}.${actualKey}`, value as string])
81
+ );
82
+ continue;
83
+ }
84
+
85
+ const field = summonByName(node.model.fields, key);
86
+ const fullKey = `${node.shortTableAlias}.${key}`;
87
+
88
+ if (field.relation) {
89
+ const relation = get(node.model.relationsByName, field.name);
90
+ const tableAlias = `${node.model.name}__W__${key}`;
91
+ const subNode: WhereNode = {
92
+ ctx: node.ctx,
93
+ model: relation.model,
94
+ tableName: relation.model.name,
95
+ tableAlias,
96
+ shortTableAlias: node.ctx.aliases.getShort(tableAlias),
97
+ foreignKey: relation.field.foreignKey,
98
+ };
99
+ addJoin(joins, node.tableAlias, subNode.tableName, subNode.tableAlias, get(subNode, 'foreignKey'), 'id');
100
+ applyWhere(subNode, value as Where, ops, joins);
101
+ continue;
102
+ }
103
+
104
+ if (Array.isArray(value)) {
105
+ if (field && field.list) {
106
+ ops.push((query) =>
107
+ ors(
108
+ query,
109
+ value.map((v) => (subQuery) => subQuery.whereRaw('? = ANY(??)', [v, fullKey] as string[]))
110
+ )
111
+ );
112
+ continue;
113
+ }
114
+
115
+ if (value.some((v) => v === null)) {
116
+ if (value.some((v) => v !== null)) {
117
+ ops.push((query) =>
118
+ ors(query, [
119
+ (subQuery) => subQuery.whereIn(fullKey, value.filter((v) => v !== null) as string[]),
120
+ (subQuery) => subQuery.whereNull(fullKey),
121
+ ])
122
+ );
123
+ continue;
124
+ }
125
+
126
+ ops.push((query) => query.whereNull(fullKey));
127
+ continue;
128
+ }
129
+
130
+ ops.push((query) => query.whereIn(fullKey, value as string[]));
131
+ continue;
132
+ }
133
+
134
+ ops.push((query) => query.where({ [fullKey]: value }));
135
+ }
136
+ };
137
+
138
+ const applySearch = (node: FieldResolverNode, search: string, query: Knex.QueryBuilder) =>
139
+ ors(
140
+ query,
141
+ node.model.fields
142
+ .filter(({ searchable }) => searchable)
143
+ .map(
144
+ ({ name }) =>
145
+ (query) =>
146
+ query.whereILike(`${node.shortTableAlias}.${name}`, `%${search}%`)
147
+ )
148
+ );
149
+
150
+ const applyOrderBy = (node: FieldResolverNode, orderBy: OrderBy, query: Knex.QueryBuilder) => {
151
+ for (const vals of orderBy) {
152
+ const keys = Object.keys(vals);
153
+ if (keys.length !== 1) {
154
+ throw new UserInputError(`You need to specify exactly 1 value to order by for each orderBy entry.`);
155
+ }
156
+ const key = keys[0];
157
+ const value = vals[key!];
158
+
159
+ // Simple field
160
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
161
+ query.orderBy(`${node.shortTableAlias}.${key}`, value);
162
+ }
163
+ };
@@ -0,0 +1,9 @@
1
+ // created from 'create-ts-index'
2
+
3
+ export * from './arguments';
4
+ export * from './filters';
5
+ export * from './mutations';
6
+ export * from './node';
7
+ export * from './resolver';
8
+ export * from './resolvers';
9
+ export * from './utils';
@@ -0,0 +1,313 @@
1
+ import { GraphQLResolveInfo } from 'graphql';
2
+ import { v4 as uuid } from 'uuid';
3
+ import { Context, FullContext } from '../context';
4
+ import { ForbiddenError, GraphQLError } from '../errors';
5
+ import { Entity, Model, ModelField, isEnumList } from '../models';
6
+ import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
7
+ import { get, it, summonByName, typeToField } from '../utils';
8
+ import { resolve } from './resolver';
9
+ import { AliasGenerator } from './utils';
10
+
11
+ export const mutationResolver = async (_parent: any, args: any, partialCtx: Context, info: GraphQLResolveInfo) => {
12
+ return await partialCtx.knex.transaction(async (knex) => {
13
+ const [, mutation, modelName] = it(info.fieldName.match(/^(create|update|delete|restore)(.+)$/));
14
+ const ctx = { ...partialCtx, knex, info, aliases: new AliasGenerator() };
15
+ const model = summonByName(ctx.models, modelName!);
16
+ switch (mutation) {
17
+ case 'create':
18
+ return await create(model, args, ctx);
19
+ case 'update':
20
+ return await update(model, args, ctx);
21
+ case 'delete':
22
+ return await del(model, args, ctx);
23
+ case 'restore':
24
+ return await restore(model, args, ctx);
25
+ }
26
+ });
27
+ };
28
+
29
+ const create = async (model: Model, { data: input }: { data: any }, ctx: FullContext) => {
30
+ const normalizedInput = { ...input };
31
+ normalizedInput.id = uuid();
32
+ normalizedInput.createdAt = ctx.now;
33
+ normalizedInput.createdById = ctx.user.id;
34
+ sanitize(ctx, model, normalizedInput);
35
+
36
+ await checkCanWrite(ctx, model, normalizedInput, 'CREATE');
37
+ await ctx.handleUploads?.(normalizedInput);
38
+
39
+ const data = { prev: {}, input, normalizedInput, next: normalizedInput };
40
+ await ctx.mutationHook?.(model, 'create', 'before', data, ctx);
41
+ await ctx.knex(model.name).insert(normalizedInput);
42
+ await createRevision(model, normalizedInput, ctx);
43
+ await ctx.mutationHook?.(model, 'create', 'after', data, ctx);
44
+
45
+ return await resolve(ctx, normalizedInput.id);
46
+ };
47
+
48
+ const update = async (model: Model, { where, data: input }: { where: any; data: any }, ctx: FullContext) => {
49
+ if (Object.keys(where).length === 0) {
50
+ throw new Error(`No ${model.name} specified.`);
51
+ }
52
+
53
+ const normalizedInput = { ...input };
54
+
55
+ sanitize(ctx, model, normalizedInput);
56
+
57
+ const prev = await getEntityToMutate(ctx, model, where, 'UPDATE');
58
+
59
+ // Remove data that wouldn't mutate given that it's irrelevant for permissions
60
+ for (const key of Object.keys(normalizedInput)) {
61
+ if (normalizedInput[key] === prev[key]) {
62
+ delete normalizedInput[key];
63
+ }
64
+ }
65
+
66
+ if (Object.keys(normalizedInput).length > 0) {
67
+ await checkCanWrite(ctx, model, normalizedInput, 'UPDATE');
68
+ await ctx.handleUploads?.(normalizedInput);
69
+
70
+ const next = { ...prev, ...normalizedInput };
71
+ const data = { prev, input, normalizedInput, next };
72
+ await ctx.mutationHook?.(model, 'update', 'before', data, ctx);
73
+ await ctx.knex(model.name).where(where).update(normalizedInput);
74
+ await createRevision(model, next, ctx);
75
+ await ctx.mutationHook?.(model, 'update', 'after', data, ctx);
76
+ }
77
+
78
+ return await resolve(ctx);
79
+ };
80
+
81
+ type Callbacks = (() => Promise<void>)[];
82
+
83
+ const del = async (model: Model, { where, dryRun }: { where: any; dryRun: boolean }, ctx: FullContext) => {
84
+ if (Object.keys(where).length === 0) {
85
+ throw new Error(`No ${model.name} specified.`);
86
+ }
87
+
88
+ const entity = await getEntityToMutate(ctx, model, where, 'DELETE');
89
+
90
+ if (entity.deleted) {
91
+ throw new ForbiddenError('Entity is already deleted.');
92
+ }
93
+
94
+ const toDelete: { [type: string]: { [id: string]: string } } = {};
95
+ const toUnlink: {
96
+ [type: string]: {
97
+ [id: string]: {
98
+ display: string;
99
+ fields: string[];
100
+ };
101
+ };
102
+ } = {};
103
+
104
+ const beforeHooks: Callbacks = [];
105
+ const mutations: Callbacks = [];
106
+ const afterHooks: Callbacks = [];
107
+
108
+ const deleteCascade = async (currentModel: Model, entity: Entity) => {
109
+ if (entity.deleted) {
110
+ return;
111
+ }
112
+
113
+ if (dryRun) {
114
+ if (!(currentModel.name in toDelete)) {
115
+ toDelete[currentModel.name] = {};
116
+ }
117
+ toDelete[currentModel.name]![entity.id] = entity[currentModel.displayField || 'id'] || entity.id;
118
+ } else {
119
+ const normalizedInput = { deleted: true, deletedAt: ctx.now, deletedById: ctx.user.id };
120
+ const data = { prev: entity, input: {}, normalizedInput, next: { ...entity, ...normalizedInput } };
121
+ if (ctx.mutationHook) {
122
+ beforeHooks.push(async () => {
123
+ await ctx.mutationHook!(currentModel, 'delete', 'before', data, ctx);
124
+ });
125
+ }
126
+ mutations.push(async () => {
127
+ await ctx.knex(currentModel.name).where({ id: entity.id }).update(normalizedInput);
128
+ await createRevision(currentModel, { ...entity, deleted: true }, ctx);
129
+ });
130
+ if (ctx.mutationHook) {
131
+ afterHooks.push(async () => {
132
+ await ctx.mutationHook!(currentModel, 'delete', 'after', data, ctx);
133
+ });
134
+ }
135
+ }
136
+
137
+ for (const {
138
+ model: descendantModel,
139
+ foreignKey,
140
+ field: { name, onDelete },
141
+ } of currentModel.reverseRelations) {
142
+ const query = ctx.knex(descendantModel.name).where({ [foreignKey]: entity.id });
143
+ switch (onDelete) {
144
+ case 'set-null': {
145
+ const descendants = await query;
146
+ for (const descendant of descendants) {
147
+ if (dryRun) {
148
+ if (!toUnlink[descendantModel.name]) {
149
+ toUnlink[descendantModel.name] = {};
150
+ }
151
+ if (!toUnlink[descendantModel.name]![descendant.id]) {
152
+ toUnlink[descendantModel.name]![descendant.id] = {
153
+ display: descendant[descendantModel.displayField || 'id'] || entity.id,
154
+ fields: [],
155
+ };
156
+ }
157
+ toUnlink[descendantModel.name]![descendant.id]!.fields.push(name);
158
+ } else {
159
+ mutations.push(async () => {
160
+ await ctx
161
+ .knex(descendantModel.name)
162
+ .where({ id: descendant.id })
163
+ .update({
164
+ [`${name}Id`]: null,
165
+ });
166
+ });
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ case 'cascade':
172
+ default: {
173
+ applyPermissions(ctx, descendantModel.name, descendantModel.name, query, 'DELETE');
174
+ const descendants = await query;
175
+ if (descendants.length && !descendantModel.deletable) {
176
+ throw new ForbiddenError(`This ${model.name} depends on a ${descendantModel.name} which cannot be deleted.`);
177
+ }
178
+ for (const descendant of descendants) {
179
+ await deleteCascade(descendantModel, descendant);
180
+ }
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ };
186
+
187
+ await deleteCascade(model, entity);
188
+
189
+ for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
190
+ await callback();
191
+ }
192
+
193
+ if (dryRun) {
194
+ throw new GraphQLError(`Delete dry run:`, {
195
+ code: 'DELETE_DRY_RUN',
196
+ toDelete,
197
+ toUnlink,
198
+ });
199
+ }
200
+
201
+ return entity.id;
202
+ };
203
+
204
+ const restore = async (model: Model, { where }: { where: any }, ctx: FullContext) => {
205
+ if (Object.keys(where).length === 0) {
206
+ throw new Error(`No ${model.name} specified.`);
207
+ }
208
+
209
+ const entity = await getEntityToMutate(ctx, model, where, 'RESTORE');
210
+
211
+ if (!entity.deleted) {
212
+ throw new ForbiddenError('Entity is not deleted.');
213
+ }
214
+
215
+ const beforeHooks: Callbacks = [];
216
+ const mutations: Callbacks = [];
217
+ const afterHooks: Callbacks = [];
218
+
219
+ const restoreCascade = async (currentModel: Model, relatedEntity: Entity) => {
220
+ if (!relatedEntity.deleted || !relatedEntity.deletedAt || !relatedEntity.deletedAt.equals(entity.deletedAt)) {
221
+ return;
222
+ }
223
+
224
+ const normalizedInput: Entity = { deleted: false, deletedAt: null, deletedById: null };
225
+ const data = { prev: relatedEntity, input: {}, normalizedInput, next: { ...relatedEntity, ...normalizedInput } };
226
+ if (ctx.mutationHook) {
227
+ beforeHooks.push(async () => {
228
+ await ctx.mutationHook!(model, 'restore', 'before', data, ctx);
229
+ });
230
+ }
231
+ mutations.push(async () => {
232
+ await ctx.knex(currentModel.name).where({ id: relatedEntity.id }).update(normalizedInput);
233
+ await createRevision(currentModel, { ...relatedEntity, deleted: false }, ctx);
234
+ });
235
+ if (ctx.mutationHook) {
236
+ afterHooks.push(async () => {
237
+ await ctx.mutationHook!(model, 'restore', 'after', data, ctx);
238
+ });
239
+ }
240
+
241
+ for (const { model: descendantModel, foreignKey } of currentModel.reverseRelations.filter(
242
+ ({ model: { deletable } }) => deletable
243
+ )) {
244
+ const query = ctx.knex(descendantModel.name).where({ [foreignKey]: relatedEntity.id });
245
+ applyPermissions(ctx, descendantModel.name, descendantModel.name, query, 'RESTORE');
246
+ const descendants = await query;
247
+ for (const descendant of descendants) {
248
+ await restoreCascade(descendantModel, descendant);
249
+ }
250
+ }
251
+ };
252
+
253
+ await restoreCascade(model, entity);
254
+
255
+ for (const callback of [...beforeHooks, ...mutations, ...afterHooks]) {
256
+ await callback();
257
+ }
258
+
259
+ return entity.id;
260
+ };
261
+
262
+ const createRevision = async (model: Model, data: Entity, ctx: Context) => {
263
+ if (model.updatable) {
264
+ const revisionData = {
265
+ id: uuid(),
266
+ [`${typeToField(model.name)}Id`]: data.id,
267
+ createdAt: ctx.now,
268
+ createdById: ctx.user.id,
269
+ };
270
+
271
+ if (model.deletable) {
272
+ revisionData.deleted = data.deleted || false;
273
+ }
274
+
275
+ for (const { name, relation, nonNull, ...field } of model.fields.filter(({ updatable }) => updatable)) {
276
+ const col = relation ? `${name}Id` : name;
277
+ if (nonNull && (!(col in data) || col === undefined || col === null)) {
278
+ revisionData[col] = get(field, 'default');
279
+ } else {
280
+ revisionData[col] = data[col];
281
+ }
282
+ }
283
+ await ctx.knex(`${model.name}Revision`).insert(revisionData);
284
+ }
285
+ };
286
+
287
+ const sanitize = (ctx: FullContext, model: Model, data: Entity) => {
288
+ if (model.updatable) {
289
+ data.updatedAt = ctx.now;
290
+ data.updatedById = ctx.user.id;
291
+ }
292
+
293
+ for (const key of Object.keys(data)) {
294
+ const field = model.fields.find(({ name }) => name === key);
295
+
296
+ if (!field) {
297
+ continue;
298
+ }
299
+
300
+ if (isEndOfDay(field) && data[key]) {
301
+ data[key] = data[key].endOf('day');
302
+ continue;
303
+ }
304
+
305
+ if (isEnumList(ctx.rawModels, field) && Array.isArray(data[key])) {
306
+ data[key] = `{${data[key].join(',')}}`;
307
+ continue;
308
+ }
309
+ }
310
+ };
311
+
312
+ const isEndOfDay = (field?: ModelField) =>
313
+ field?.endOfDay === true && field?.dateTimeType === 'date' && field?.type === 'DateTime';