@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
package/src/models.ts ADDED
@@ -0,0 +1,228 @@
1
+ import type { Context } from './context';
2
+ import type { OrderBy } from './resolvers/arguments';
3
+ import type { Directive, Value } from './values';
4
+
5
+ export type RawModels = RawModel[];
6
+
7
+ export type RawModel =
8
+ | ScalarModel
9
+ | EnumModel
10
+ | RawEnumModel
11
+ | InterfaceModel
12
+ | ObjectModel
13
+ | RawObjectModel
14
+ | JsonObjectModel;
15
+
16
+ type BaseModel = {
17
+ name: string;
18
+ plural?: string;
19
+ description?: string;
20
+ };
21
+
22
+ export type ScalarModel = BaseModel & { type: 'scalar' };
23
+
24
+ export type EnumModel = BaseModel & { type: 'enum'; values: string[]; deleted?: true };
25
+
26
+ export type RawEnumModel = BaseModel & { type: 'raw-enum'; values: string[] };
27
+
28
+ export type InterfaceModel = BaseModel & { type: 'interface'; fields: ModelField[] };
29
+
30
+ export type RawObjectModel = BaseModel & {
31
+ type: 'raw-object';
32
+ fields: ModelField[];
33
+ rawFilters?: { name: string; type: string; list?: boolean; nonNull?: boolean }[];
34
+ };
35
+
36
+ export type JsonObjectModel = BaseModel & {
37
+ type: 'json-object';
38
+ json: true;
39
+ fields: Pick<ModelField, 'type' | 'name' | 'nonNull'>[];
40
+ };
41
+
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- data is derived from the models
43
+ export type Entity = Record<string, any>;
44
+
45
+ export type Action = 'create' | 'update' | 'delete' | 'restore';
46
+
47
+ export type MutationHook = (
48
+ model: Model,
49
+ action: Action,
50
+ when: 'before' | 'after',
51
+ data: { prev: Entity; input: Entity; normalizedInput: Entity; next: Entity },
52
+ ctx: Context
53
+ ) => Promise<void>;
54
+
55
+ export type ObjectModel = BaseModel & {
56
+ type: 'object';
57
+ interfaces?: string[];
58
+ // createdAt, createdBy, updatedAt, updatedBy can be null
59
+ nonStrict?: boolean;
60
+ queriable?: boolean;
61
+ listQueriable?: boolean;
62
+ creatable?: boolean;
63
+ updatable?: boolean;
64
+ deletable?: boolean;
65
+ displayField?: string;
66
+ defaultOrderBy?: OrderBy;
67
+ fields: ModelField[];
68
+
69
+ // temporary fields for the generation of migrations
70
+ deleted?: true;
71
+ oldName?: string;
72
+ };
73
+
74
+ export type InputObject = {
75
+ name: string;
76
+ type: string;
77
+ nonNull?: boolean;
78
+ };
79
+
80
+ export const isObjectModel = (model: RawModel): model is ObjectModel => model.type === 'object';
81
+
82
+ export const isEnumModel = (model: RawModel): model is EnumModel => model.type === 'enum';
83
+
84
+ export const isRawEnumModel = (model: RawModel): model is RawEnumModel => model.type === 'raw-enum';
85
+
86
+ export const isScalarModel = (model: RawModel): model is ScalarModel => model.type === 'scalar';
87
+
88
+ export const isRawObjectModel = (model: RawModel): model is RawObjectModel => model.type === 'raw-object';
89
+
90
+ export const isJsonObjectModel = (model: RawModel): model is RawObjectModel => model.type === 'json-object';
91
+
92
+ export const isEnumList = (models: RawModels, field: ModelField) =>
93
+ field?.list === true && models.find(({ name }) => name === field.type)?.type === 'enum';
94
+
95
+ export const and =
96
+ (...predicates: ((field: ModelField) => boolean)[]) =>
97
+ (field: ModelField) =>
98
+ predicates.every((predicate) => predicate(field));
99
+
100
+ export const not = (predicate: (field: ModelField) => boolean) => (field: ModelField) => !predicate(field);
101
+
102
+ export const isRelation = ({ relation }: ModelField) => !!relation;
103
+
104
+ export type VisibleRelationsByRole = Record<string, Record<string, string[]>>;
105
+
106
+ export const isVisibleRelation = (visibleRelationsByRole: VisibleRelationsByRole, modelName: string, role: string) => {
107
+ const whitelist = visibleRelationsByRole[role]?.[modelName];
108
+ return ({ name }: Field) => (whitelist ? whitelist.includes(name) : true);
109
+ };
110
+
111
+ export const isToOneRelation = ({ toOne }: ModelField) => !!toOne;
112
+
113
+ export const isQueriableField = ({ queriable }: ModelField) => queriable !== false;
114
+
115
+ export const isRaw = ({ raw }: ModelField) => !!raw;
116
+
117
+ export const isVisible = ({ hidden }: ModelField) => hidden !== true;
118
+
119
+ export const isSimpleField = and(not(isRelation), not(isRaw));
120
+
121
+ export const isUpdatable = ({ updatable }: ModelField) => !!updatable;
122
+
123
+ export const isCreatable = ({ creatable }: ModelField) => !!creatable;
124
+
125
+ export const isQueriableBy = (role: string) => (field: ModelField) =>
126
+ isQueriableField(field) && (!field.queriableBy || field.queriableBy.includes(role));
127
+
128
+ export const isUpdatableBy = (role: string) => (field: ModelField) =>
129
+ isUpdatable(field) && (!field.updatableBy || field.updatableBy.includes(role));
130
+
131
+ export const isCreatableBy = (role: string) => (field: ModelField) =>
132
+ isCreatable(field) && (!field.creatableBy || field.creatableBy.includes(role));
133
+
134
+ export const actionableRelations = (model: Model, action: 'create' | 'update' | 'filter') =>
135
+ model.fields.filter(
136
+ ({ relation, ...field }) =>
137
+ relation &&
138
+ field[`${action === 'filter' ? action : action.slice(0, -1)}able` as 'filterable' | 'creatable' | 'updatable']
139
+ );
140
+
141
+ export type Field = {
142
+ name: string;
143
+ type: string;
144
+ default?: Value;
145
+ list?: boolean;
146
+ nonNull?: boolean;
147
+ args?: Field[];
148
+ directives?: Directive[];
149
+ };
150
+
151
+ export type ModelField = Field & {
152
+ primary?: boolean;
153
+ unique?: boolean;
154
+ filterable?: boolean;
155
+ defaultFilter?: Value;
156
+ searchable?: boolean;
157
+ possibleValues?: Value[];
158
+ orderable?: boolean;
159
+ comparable?: boolean;
160
+ relation?: boolean;
161
+ onDelete?: 'cascade' | 'set-null';
162
+ reverse?: string;
163
+ toOne?: boolean;
164
+ foreignKey?: string;
165
+ queriable?: false;
166
+ queriableBy?: string[];
167
+ creatable?: boolean;
168
+ creatableBy?: string[];
169
+ updatable?: boolean;
170
+ updatableBy?: string[];
171
+ generated?: boolean;
172
+ raw?: boolean;
173
+ json?: boolean;
174
+ dateTimeType?: 'year' | 'date' | 'datetime' | 'year_and_month';
175
+ stringType?: 'email' | 'url' | 'phone';
176
+ floatType?: 'currency' | 'percentage';
177
+ unit?: 'million';
178
+ intType?: 'currency';
179
+ min?: number;
180
+ max?: number;
181
+ // The tooltip is "hidden" behind an icon in the admin forms
182
+ tooltip?: string;
183
+ // The description is always visible below the inputs in the admin forms
184
+ description?: string;
185
+ large?: true;
186
+ maxLength?: number;
187
+ double?: boolean;
188
+ precision?: number;
189
+ scale?: number;
190
+ defaultValue?: string | number | ReadonlyArray<string> | undefined;
191
+ endOfDay?: boolean;
192
+ obfuscate?: true;
193
+ // If true the field must be filled within forms but can be null in the database
194
+ required?: boolean;
195
+ indent?: boolean;
196
+ // If true the field is hidden in the admin interface
197
+ hidden?: boolean;
198
+
199
+ // temporary fields for the generation of migrations
200
+ deleted?: true;
201
+ oldName?: string;
202
+ };
203
+
204
+ export type Models = Model[];
205
+
206
+ export type Model = ObjectModel & {
207
+ fieldsByName: Record<string, ModelField>;
208
+ relations: Relation[];
209
+ relationsByName: Record<string, Relation>;
210
+ reverseRelations: ReverseRelation[];
211
+ reverseRelationsByName: Record<string, ReverseRelation>;
212
+ };
213
+
214
+ export type Relation = {
215
+ field: ModelField;
216
+ model: Model;
217
+ reverseRelation: ReverseRelation;
218
+ };
219
+
220
+ export type ReverseRelation = {
221
+ name: string;
222
+ type: string;
223
+ foreignKey: string;
224
+ toOne: boolean;
225
+ model: Model;
226
+ field: ModelField;
227
+ fieldModel: Model;
228
+ };
@@ -0,0 +1,239 @@
1
+ import { Knex } from 'knex';
2
+ import { FullContext } from '../context';
3
+ import { NotFoundError, PermissionError } from '../errors';
4
+ import { Model } from '../models';
5
+ import { AliasGenerator, hash, ors } from '../resolvers/utils';
6
+ import { get, getModelPlural, summonByName } from '../utils';
7
+ import { BasicValue } from '../values';
8
+ import { PermissionAction, PermissionLink, PermissionStack } from './generate';
9
+
10
+ export const getPermissionStack = (
11
+ ctx: Pick<FullContext, 'permissions' | 'user'>,
12
+ type: string,
13
+ action: PermissionAction
14
+ ): boolean | PermissionStack => {
15
+ const rolePermissions = ctx.permissions[ctx.user.role];
16
+ if (typeof rolePermissions === 'boolean' || rolePermissions === undefined) {
17
+ return !!rolePermissions;
18
+ }
19
+
20
+ const typePermissions = rolePermissions[type];
21
+ if (typeof typePermissions === 'boolean' || typePermissions === undefined) {
22
+ return !!typePermissions;
23
+ }
24
+
25
+ const actionPermission = typePermissions[action];
26
+ if (typeof actionPermission === 'boolean' || actionPermission === undefined) {
27
+ return !!actionPermission;
28
+ }
29
+
30
+ return actionPermission;
31
+ };
32
+
33
+ export const applyPermissions = (
34
+ ctx: Pick<FullContext, 'models' | 'permissions' | 'user' | 'knex'>,
35
+ type: string,
36
+ tableAlias: string,
37
+ query: Knex.QueryBuilder,
38
+ action: PermissionAction,
39
+ verifiedPermissionStack?: PermissionStack
40
+ ): boolean | PermissionStack => {
41
+ const permissionStack = getPermissionStack(ctx, type, action);
42
+
43
+ if (permissionStack === true) {
44
+ return permissionStack;
45
+ }
46
+
47
+ if (permissionStack === false) {
48
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
49
+ query.where(false);
50
+ return permissionStack;
51
+ }
52
+
53
+ if (
54
+ verifiedPermissionStack?.every((prefixChain) =>
55
+ permissionStack.some(
56
+ (chain) =>
57
+ hash(prefixChain) === hash(chain.slice(0, -1)) &&
58
+ // TODO: this is stricter than it could be if we add these checks to the query
59
+ !('where' in get(chain, chain.length - 1)) &&
60
+ !('me' in get(chain, chain.length - 1))
61
+ )
62
+ )
63
+ ) {
64
+ // The user has access to a parent entity with one or more from a set of rules, all of which are inherited by this entity
65
+ // No need for additional checks
66
+ return permissionStack;
67
+ }
68
+
69
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
70
+ ors(
71
+ query,
72
+ permissionStack.map(
73
+ (links) => (query) =>
74
+ query
75
+ .whereNull(`${tableAlias}.id`)
76
+ .orWhereExists((subQuery) => permissionLinkQuery(ctx, subQuery, links, ctx.knex.raw(`"${tableAlias}".id`)))
77
+ )
78
+ );
79
+
80
+ return permissionStack;
81
+ };
82
+
83
+ /**
84
+ * Check whether entity as currently in db can be mutated (update or delete)
85
+ */
86
+ export const getEntityToMutate = async (
87
+ ctx: Pick<FullContext, 'models' | 'permissions' | 'user' | 'knex'>,
88
+ model: Model,
89
+ where: Record<string, BasicValue>,
90
+ action: 'UPDATE' | 'DELETE' | 'RESTORE'
91
+ ) => {
92
+ const query = ctx.knex(model.name).where(where).first();
93
+ let entity = await query.clone();
94
+
95
+ if (!entity) {
96
+ throw new NotFoundError('Entity to mutate');
97
+ }
98
+
99
+ applyPermissions(ctx, model.name, model.name, query, action);
100
+ entity = await query;
101
+ if (!entity) {
102
+ throw new PermissionError(action, `this ${model.name}`);
103
+ }
104
+
105
+ return entity;
106
+ };
107
+
108
+ /**
109
+ * Check whether given data can be written to db (insert or update)
110
+ */
111
+ export const checkCanWrite = async (
112
+ ctx: Pick<FullContext, 'models' | 'permissions' | 'user' | 'knex'>,
113
+ model: Model,
114
+ data: Record<string, BasicValue>,
115
+ action: 'CREATE' | 'UPDATE'
116
+ ) => {
117
+ const permissionStack = getPermissionStack(ctx, model.name, action);
118
+
119
+ if (permissionStack === true) {
120
+ return;
121
+ }
122
+ if (permissionStack === false) {
123
+ throw new PermissionError(action, getModelPlural(model));
124
+ }
125
+
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- using `select(1 as any)` to instantiate an "empty" query builder
127
+ const query = ctx.knex.select(1 as any).first();
128
+ let linked = false;
129
+
130
+ for (const field of model.fields
131
+ .filter(({ relation }) => relation)
132
+ .filter((field) => field.generated || (action === 'CREATE' ? field.creatable : field.updatable))) {
133
+ const foreignKey = field.foreignKey || `${field.name}Id`;
134
+ const foreignId = data[foreignKey] as string;
135
+ if (!foreignId) {
136
+ continue;
137
+ }
138
+
139
+ const fieldPermissions = field[action === 'CREATE' ? 'creatableBy' : 'updatableBy'];
140
+ if (fieldPermissions && !fieldPermissions.includes(ctx.user.role)) {
141
+ throw new PermissionError(action, `this ${model.name}'s ${field.name}`);
142
+ }
143
+
144
+ linked = true;
145
+
146
+ const fieldPermissionStack = getPermissionStack(ctx, field.type, 'LINK');
147
+
148
+ if (fieldPermissionStack === true) {
149
+ // User can link any entity from this type, just check whether it exists
150
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
151
+ query.whereExists((subQuery) => subQuery.from(`${field.type} as a`).whereRaw(`a.id = ?`, foreignId));
152
+ continue;
153
+ }
154
+
155
+ if (fieldPermissionStack === false || !fieldPermissionStack.length) {
156
+ throw new PermissionError(action, `this ${model.name}'s ${field.name}`);
157
+ }
158
+
159
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
160
+ ors(
161
+ query,
162
+ fieldPermissionStack.map(
163
+ (links) => (query) => query.whereExists((subQuery) => permissionLinkQuery(ctx, subQuery, links, foreignId))
164
+ )
165
+ );
166
+ }
167
+
168
+ if (linked) {
169
+ const canMutate = await query;
170
+ if (!canMutate) {
171
+ throw new PermissionError(action, `this ${model.name} because there are no entities you can link it to`);
172
+ }
173
+ } else if (action === 'CREATE') {
174
+ throw new PermissionError(action, `this ${model.name} because there are no entity types you can link it to`);
175
+ }
176
+ };
177
+
178
+ const permissionLinkQuery = (
179
+ ctx: Pick<FullContext, 'models' | 'user'>,
180
+ subQuery: Knex.QueryBuilder,
181
+ links: PermissionLink[],
182
+ id: Knex.RawBinding | Knex.ValueDict
183
+ ) => {
184
+ const aliases = new AliasGenerator();
185
+ let alias = aliases.getShort();
186
+ const { type, me, where } = links[0]!;
187
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
188
+ subQuery.from(`${type} as ${alias}`);
189
+ if (me) {
190
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
191
+ subQuery.where({ [`${alias}.id`]: ctx.user.id });
192
+ }
193
+ if (where) {
194
+ applyWhere(summonByName(ctx.models, type), subQuery, alias, where, aliases);
195
+ }
196
+
197
+ for (const { type, foreignKey, reverse, where } of links) {
198
+ const model = summonByName(ctx.models, type);
199
+ const subAlias = aliases.getShort();
200
+ if (reverse) {
201
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
202
+ subQuery.leftJoin(`${type} as ${subAlias}`, `${alias}.${foreignKey || 'id'}`, `${subAlias}.id`);
203
+ } else {
204
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
205
+ subQuery.rightJoin(`${type} as ${subAlias}`, `${alias}.id`, `${subAlias}.${foreignKey || 'id'}`);
206
+ }
207
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
208
+ subQuery.where({ [`${subAlias}.deleted`]: false });
209
+ if (where) {
210
+ applyWhere(model, subQuery, subAlias, where, aliases);
211
+ }
212
+ alias = subAlias;
213
+ }
214
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
215
+ subQuery.whereRaw(`"${alias}".id = ?`, id);
216
+ };
217
+
218
+ const applyWhere = (model: Model, query: Knex.QueryBuilder, alias: string, where: any, aliases: AliasGenerator) => {
219
+ for (const [key, value] of Object.entries(where)) {
220
+ const relation = model.relationsByName[key];
221
+
222
+ if (relation) {
223
+ const subAlias = aliases.getShort();
224
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
225
+ query.leftJoin(
226
+ `${relation.model.name} as ${subAlias}`,
227
+ `${alias}.${relation.field.foreignKey || `${relation.field.name}Id`}`,
228
+ `${subAlias}.id`
229
+ );
230
+ applyWhere(relation.model, query, subAlias, value, aliases);
231
+ } else if (Array.isArray(value)) {
232
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
233
+ query.whereIn(`${alias}.${key}`, value);
234
+ } else {
235
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises -- we do not need to await knex here
236
+ query.where({ [`${alias}.${key}`]: value });
237
+ }
238
+ }
239
+ };
@@ -0,0 +1,143 @@
1
+ import { Models } from '../models';
2
+ import { summonByName } from '../utils';
3
+
4
+ export type PermissionAction = 'READ' | 'CREATE' | 'UPDATE' | 'DELETE' | 'RESTORE' | 'LINK';
5
+
6
+ const ACTIONS: PermissionAction[] = ['READ', 'CREATE', 'UPDATE', 'DELETE', 'RESTORE', 'LINK'];
7
+
8
+ /**
9
+ * Initial representation (tree structure, as defined by user).
10
+ */
11
+ export type PermissionsConfig = {
12
+ [role: string]:
13
+ | true
14
+ | {
15
+ [type: string]: PermissionsBlock;
16
+ };
17
+ };
18
+
19
+ export type PermissionsBlock = {
20
+ [action in PermissionAction]?: true;
21
+ } & {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ WHERE?: Record<string, any>;
24
+ RELATIONS?: {
25
+ [relation: string]: PermissionsBlock;
26
+ };
27
+ };
28
+
29
+ /**
30
+ * Final representation (lookup table (role, model, action) -> permission stack).
31
+ */
32
+ export type Permissions = {
33
+ [role: string]: true | RolePermissions;
34
+ };
35
+
36
+ type RolePermissions = {
37
+ [type: string]: {
38
+ [action in PermissionAction]?: true | PermissionStack;
39
+ };
40
+ };
41
+
42
+ /**
43
+ * For a given role, model and action,
44
+ * this represents the list of potential (join) paths
45
+ * that would grant permission to perform that action.
46
+ */
47
+ export type PermissionStack = PermissionChain[];
48
+
49
+ export type PermissionChain = PermissionLink[];
50
+
51
+ export type PermissionLink = {
52
+ type: string;
53
+ foreignKey?: string;
54
+ reverse?: boolean;
55
+ me?: boolean;
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ where?: any;
58
+ };
59
+
60
+ export const generatePermissions = (models: Models, config: PermissionsConfig) => {
61
+ const permissions: Permissions = {};
62
+ for (const [role, roleConfig] of Object.entries(config)) {
63
+ if (roleConfig === true) {
64
+ permissions[role] = true;
65
+ continue;
66
+ }
67
+ const rolePermissions: RolePermissions = {};
68
+ for (const [key, block] of Object.entries(roleConfig)) {
69
+ const type = key === 'me' ? 'User' : key;
70
+ if (key !== 'me' && !('WHERE' in block)) {
71
+ rolePermissions[type] = {};
72
+ for (const action of ACTIONS) {
73
+ if (action === 'READ' || action in block) {
74
+ rolePermissions[type]![action] = true;
75
+ }
76
+ }
77
+ }
78
+ addPermissions(
79
+ models,
80
+ rolePermissions,
81
+ [
82
+ {
83
+ type,
84
+ ...(key === 'me' && { me: true }),
85
+ ...('WHERE' in block && { where: block.WHERE }),
86
+ },
87
+ ],
88
+ block
89
+ );
90
+ }
91
+ permissions[role] = rolePermissions;
92
+ }
93
+
94
+ return permissions;
95
+ };
96
+
97
+ const addPermissions = (models: Models, permissions: RolePermissions, links: PermissionLink[], block: PermissionsBlock) => {
98
+ const { type } = links[links.length - 1]!;
99
+ const model = summonByName(models, type);
100
+
101
+ for (const action of ACTIONS) {
102
+ if (action === 'READ' || action in block) {
103
+ if (!permissions[type]) {
104
+ permissions[type] = {};
105
+ }
106
+ if (!permissions[type]![action]) {
107
+ permissions[type]![action] = [];
108
+ }
109
+ if (permissions[type]![action] !== true) {
110
+ (permissions[type]![action] as PermissionStack).push(links);
111
+ }
112
+ }
113
+ }
114
+
115
+ if (block.RELATIONS) {
116
+ for (const [relation, subBlock] of Object.entries(block.RELATIONS)) {
117
+ const field = model.fields.find((field) => field.relation && field.name === relation);
118
+ let link: PermissionLink;
119
+ if (field) {
120
+ link = {
121
+ type: field.type,
122
+ foreignKey: field.foreignKey || `${field.name}Id`,
123
+ reverse: true,
124
+ };
125
+ } else {
126
+ const field = model.reverseRelationsByName[relation]!;
127
+
128
+ if (!field) {
129
+ throw new Error(`Relation ${relation} in model ${model.name} does not exist.`);
130
+ }
131
+
132
+ link = {
133
+ type: field.model.name,
134
+ foreignKey: field.foreignKey,
135
+ };
136
+ }
137
+ if (subBlock.WHERE) {
138
+ link.where = subBlock.WHERE;
139
+ }
140
+ addPermissions(models, permissions, [...links, link], subBlock);
141
+ }
142
+ }
143
+ };
@@ -0,0 +1,4 @@
1
+ // created from 'create-ts-index'
2
+
3
+ export * from './check';
4
+ export * from './generate';