@smartive/graphql-magic 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +1 -6
  2. package/dist/cjs/index.cjs +196 -170
  3. package/dist/esm/client/queries.d.ts +4 -1
  4. package/dist/esm/client/queries.js +7 -4
  5. package/dist/esm/client/queries.js.map +1 -1
  6. package/dist/esm/db/generate.js +21 -13
  7. package/dist/esm/db/generate.js.map +1 -1
  8. package/dist/esm/generate/generate.js +17 -22
  9. package/dist/esm/generate/generate.js.map +1 -1
  10. package/dist/esm/generate/utils.d.ts +10 -1
  11. package/dist/esm/generate/utils.js.map +1 -1
  12. package/dist/esm/migrations/generate.js +82 -89
  13. package/dist/esm/migrations/generate.js.map +1 -1
  14. package/dist/esm/models.d.ts +115 -93
  15. package/dist/esm/models.js +1 -26
  16. package/dist/esm/models.js.map +1 -1
  17. package/dist/esm/permissions/check.js +2 -2
  18. package/dist/esm/permissions/check.js.map +1 -1
  19. package/dist/esm/permissions/generate.js +2 -2
  20. package/dist/esm/permissions/generate.js.map +1 -1
  21. package/dist/esm/resolvers/filters.js +1 -1
  22. package/dist/esm/resolvers/filters.js.map +1 -1
  23. package/dist/esm/resolvers/mutations.js +5 -6
  24. package/dist/esm/resolvers/mutations.js.map +1 -1
  25. package/dist/esm/resolvers/node.js +4 -5
  26. package/dist/esm/resolvers/node.js.map +1 -1
  27. package/dist/esm/resolvers/resolver.js +2 -2
  28. package/dist/esm/resolvers/resolver.js.map +1 -1
  29. package/dist/esm/utils.d.ts +181 -1
  30. package/dist/esm/utils.js +69 -21
  31. package/dist/esm/utils.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/client/queries.ts +16 -10
  34. package/src/db/generate.ts +22 -15
  35. package/src/generate/generate.ts +26 -34
  36. package/src/generate/utils.ts +11 -1
  37. package/src/migrations/generate.ts +84 -82
  38. package/src/models.ts +113 -157
  39. package/src/permissions/check.ts +2 -2
  40. package/src/permissions/generate.ts +2 -2
  41. package/src/resolvers/filters.ts +1 -1
  42. package/src/resolvers/mutations.ts +10 -9
  43. package/src/resolvers/node.ts +6 -6
  44. package/src/resolvers/resolver.ts +2 -2
  45. package/src/utils.ts +158 -57
  46. package/tests/utils/models.ts +7 -6
@@ -4,8 +4,8 @@ import { SchemaInspector } from 'knex-schema-inspector';
4
4
  import { Column } from 'knex-schema-inspector/dist/types/column';
5
5
  import { SchemaInspector as SchemaInspectorType } from 'knex-schema-inspector/dist/types/schema-inspector';
6
6
  import lowerFirst from 'lodash/lowerFirst';
7
- import { EnumModel, isEnumModel, Model, ModelField, Models, RawModels } from '../models';
8
- import { get, getModels, summonByName, typeToField } from '../utils';
7
+ import { EnumModel, Model, ModelField, Models, RawModels } from '../models';
8
+ import { get, getModels, isEnumModel, summonByName, typeToField } from '../utils';
9
9
  import { Value } from '../values';
10
10
 
11
11
  type Callbacks = (() => void)[];
@@ -130,20 +130,23 @@ export class MigrationGenerator {
130
130
  this.createFields(
131
131
  model,
132
132
  model.fields.filter(
133
- ({ name, relation, raw, foreignKey }) =>
134
- !raw && !this.columns[model.name].some((col) => col.name === (foreignKey || (relation ? `${name}Id` : name)))
133
+ ({ name, ...field }) =>
134
+ field.type !== 'raw' &&
135
+ !this.columns[model.name].some(
136
+ (col) => col.name === (field.type === 'relation' ? field.foreignKey || `${name}Id` : name)
137
+ )
135
138
  ),
136
139
  up,
137
140
  down
138
141
  );
139
142
 
140
143
  // Update fields
141
- const existingFields = model.fields.filter(({ name, relation, nonNull }) => {
142
- const col = this.columns[model.name].find((col) => col.name === (relation ? `${name}Id` : name));
144
+ const existingFields = model.fields.filter(({ name, type, nonNull }) => {
145
+ const col = this.columns[model.name].find((col) => col.name === (type === 'relation' ? `${name}Id` : name));
143
146
  if (!col) {
144
147
  return false;
145
148
  }
146
- return !model.nonStrict && !nonNull && !col.is_nullable;
149
+ return !nonNull && !col.is_nullable;
147
150
  });
148
151
  this.updateFields(model, existingFields, up, down);
149
152
  }
@@ -174,8 +177,8 @@ export class MigrationGenerator {
174
177
  writer.writeLine(`deleted: row.deleted,`);
175
178
  }
176
179
 
177
- for (const { name, relation } of model.fields.filter(({ updatable }) => updatable)) {
178
- const col = relation ? `${name}Id` : name;
180
+ for (const { name, type } of model.fields.filter(({ updatable }) => updatable)) {
181
+ const col = type === 'relation' ? `${name}Id` : name;
179
182
 
180
183
  writer.writeLine(`${col}: row.${col},`);
181
184
  }
@@ -194,21 +197,25 @@ export class MigrationGenerator {
194
197
  } else {
195
198
  const revisionTable = `${model.name}Revision`;
196
199
  const missingRevisionFields = model.fields.filter(
197
- ({ name, relation, raw, foreignKey, updatable }) =>
198
- !raw &&
200
+ ({ name, updatable, ...field }) =>
201
+ field.type !== 'raw' &&
199
202
  updatable &&
200
- !this.columns[revisionTable].some((col) => col.name === (foreignKey || (relation ? `${name}Id` : name)))
203
+ !this.columns[revisionTable].some(
204
+ (col) => col.name === (field.type === 'relation' ? field.foreignKey || `${name}Id` : name)
205
+ )
201
206
  );
202
207
 
203
208
  this.createRevisionFields(model, missingRevisionFields, up, down);
204
209
 
205
210
  const revisionFieldsToRemove = model.fields.filter(
206
- ({ name, updatable, foreignKey, relation, raw, generated }) =>
211
+ ({ name, updatable, generated, ...field }) =>
207
212
  !generated &&
208
- !raw &&
213
+ field.type !== 'raw' &&
209
214
  !updatable &&
210
- foreignKey !== 'id' &&
211
- this.columns[revisionTable].some((col) => col.name === (foreignKey || (relation ? `${name}Id` : name)))
215
+ !(field.type === 'relation' && field.foreignKey === 'id') &&
216
+ this.columns[revisionTable].some(
217
+ (col) => col.name === (field.type === 'relation' ? field.foreignKey || `${name}Id` : name)
218
+ )
212
219
  );
213
220
  this.createRevisionFields(model, revisionFieldsToRemove, down, up);
214
221
  }
@@ -269,8 +276,8 @@ export class MigrationGenerator {
269
276
  for (const field of fields) {
270
277
  this.alterTable(model.name, () => {
271
278
  this.renameColumn(
272
- field.relation ? `${field.oldName}Id` : get(field, 'oldName'),
273
- field.relation ? `${field.name}Id` : field.name
279
+ field.type === 'relation' ? `${field.oldName}Id` : get(field, 'oldName'),
280
+ field.type === 'relation' ? `${field.name}Id` : field.name
274
281
  );
275
282
  });
276
283
  }
@@ -280,8 +287,8 @@ export class MigrationGenerator {
280
287
  for (const field of fields) {
281
288
  this.alterTable(model.name, () => {
282
289
  this.renameColumn(
283
- field.relation ? `${field.name}Id` : field.name,
284
- field.relation ? `${field.oldName}Id` : get(field, 'oldName')
290
+ field.type === 'relation' ? `${field.name}Id` : field.name,
291
+ field.type === 'relation' ? `${field.oldName}Id` : get(field, 'oldName')
285
292
  );
286
293
  });
287
294
  }
@@ -289,9 +296,8 @@ export class MigrationGenerator {
289
296
 
290
297
  for (const field of fields) {
291
298
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
292
- summonByName(this.columns[model.name]!, field.relation ? `${field.oldName!}Id` : field.oldName!).name = field.relation
293
- ? `${field.name}Id`
294
- : field.name;
299
+ summonByName(this.columns[model.name]!, field.type === 'relation' ? `${field.oldName!}Id` : field.oldName!).name =
300
+ field.type === 'relation' ? `${field.name}Id` : field.name;
295
301
  }
296
302
  }
297
303
 
@@ -337,8 +343,8 @@ export class MigrationGenerator {
337
343
 
338
344
  down.push(() => {
339
345
  this.alterTable(model.name, () => {
340
- for (const { relation, name } of fields) {
341
- this.dropColumn(relation ? `${name}Id` : name);
346
+ for (const { type, name } of fields) {
347
+ this.dropColumn(type === 'relation' ? `${name}Id` : name);
342
348
  }
343
349
  });
344
350
  });
@@ -363,7 +369,7 @@ export class MigrationGenerator {
363
369
  this.column(
364
370
  field,
365
371
  { alter: true },
366
- summonByName(this.columns[model.name], field.relation ? `${field.name}Id` : field.name)
372
+ summonByName(this.columns[model.name], field.type === 'relation' ? `${field.name}Id` : field.name)
367
373
  );
368
374
  }
369
375
  });
@@ -389,7 +395,7 @@ export class MigrationGenerator {
389
395
  this.column(
390
396
  field,
391
397
  { alter: true },
392
- summonByName(this.columns[model.name], field.relation ? `${field.name}Id` : field.name)
398
+ summonByName(this.columns[model.name], field.type === 'relation' ? `${field.name}Id` : field.name)
393
399
  );
394
400
  }
395
401
  });
@@ -405,9 +411,7 @@ export class MigrationGenerator {
405
411
  writer.writeLine(`table.uuid('id').notNullable().primary();`);
406
412
  writer.writeLine(`table.uuid('${typeToField(model.name)}Id').notNullable();`);
407
413
  writer.write(`table.uuid('createdById')`);
408
- if (!model.nonStrict) {
409
- writer.write('.notNullable()');
410
- }
414
+ writer.write('.notNullable()');
411
415
  writer.write(';').newLine();
412
416
  writer.writeLine(`table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now(0));`);
413
417
  if (model.deletable) {
@@ -435,8 +439,8 @@ export class MigrationGenerator {
435
439
  this.writer
436
440
  .write(`await knex('${model.name}Revision').update(`)
437
441
  .inlineBlock(() => {
438
- for (const { name, relation } of missingRevisionFields) {
439
- const col = relation ? `${name}Id` : name;
442
+ for (const { name, type } of missingRevisionFields) {
443
+ const col = type === 'relation' ? `${name}Id` : name;
440
444
  this.writer
441
445
  .write(
442
446
  `${col}: knex.raw('(select "${col}" from "${model.name}" where "${model.name}".id = "${
@@ -463,7 +467,7 @@ export class MigrationGenerator {
463
467
  down.push(() => {
464
468
  this.alterTable(revisionTable, () => {
465
469
  for (const field of missingRevisionFields) {
466
- this.dropColumn(field.relation ? `${field.name}Id` : field.name);
470
+ this.dropColumn(field.type === 'relation' ? `${field.name}Id` : field.name);
467
471
  }
468
472
  });
469
473
  });
@@ -538,7 +542,7 @@ export class MigrationGenerator {
538
542
  }
539
543
 
540
544
  private column(
541
- { name, relation, type, primary, list, ...field }: ModelField,
545
+ { name, primary, list, ...field }: ModelField,
542
546
  { setUnique = true, setNonNull = true, alter = false, foreign = true, setDefault = true } = {},
543
547
  toColumn?: Column
544
548
  ) {
@@ -574,60 +578,58 @@ export class MigrationGenerator {
574
578
  }
575
579
  this.writer.write(';').newLine();
576
580
  };
577
- if (relation) {
578
- col(`table.uuid('${name}Id')`);
579
- if (foreign && !alter) {
580
- this.writer.writeLine(`table.foreign('${name}Id').references('id').inTable('${type}');`);
581
- }
582
- } else if (this.rawModels.some((m) => m.name === type && m.type === 'enum')) {
583
- list
584
- ? this.writer.write(`table.specificType('${name}', '"${typeToField(type)}"[]');`)
585
- : this.writer
581
+ switch (field.type) {
582
+ case 'Boolean':
583
+ col(`table.boolean('${name}')`);
584
+ break;
585
+ case 'Int':
586
+ col(`table.integer('${name}')`);
587
+ break;
588
+ case 'Float':
589
+ if (field.double) {
590
+ col(`table.double('${name}')`);
591
+ } else {
592
+ col(`table.decimal('${name}', ${get(field, 'precision')}, ${get(field, 'scale')})`);
593
+ }
594
+ break;
595
+ case 'String':
596
+ if (field.large) {
597
+ col(`table.text('${name}')`);
598
+ } else {
599
+ col(`table.string('${name}', ${field.maxLength})`);
600
+ }
601
+ break;
602
+ case 'DateTime':
603
+ col(`table.timestamp('${name}')`);
604
+ break;
605
+ case 'ID':
606
+ col(`table.uuid('${name}')`);
607
+ break;
608
+ case 'Upload':
609
+ break;
610
+ case 'relation':
611
+ col(`table.uuid('${name}Id')`);
612
+ if (foreign && !alter) {
613
+ this.writer.writeLine(`table.foreign('${name}Id').references('id').inTable('${field.typeName}');`);
614
+ }
615
+ break;
616
+ case 'enum':
617
+ if (list) {
618
+ this.writer.write(`table.specificType('${name}', '"${typeToField(field.type)}"[]');`);
619
+ } else {
620
+ this.writer
586
621
  .write(`table.enum('${name}', null as any, `)
587
622
  .inlineBlock(() => {
588
623
  this.writer.writeLine(`useNative: true,`);
589
624
  this.writer.writeLine(`existingType: true,`);
590
- this.writer.writeLine(`enumName: '${typeToField(type)}',`);
625
+ this.writer.writeLine(`enumName: '${typeToField(field.type)}',`);
591
626
  })
592
627
  .write(')');
593
- col();
594
- } else {
595
- switch (type) {
596
- case 'Boolean':
597
- col(`table.boolean('${name}')`);
598
- break;
599
- case 'Int':
600
- col(`table.integer('${name}')`);
601
- break;
602
- case 'Float':
603
- if (field.double) {
604
- col(`table.double('${name}')`);
605
- } else {
606
- col(`table.decimal('${name}', ${get(field, 'precision')}, ${get(field, 'scale')})`);
607
- }
608
- break;
609
- case 'String':
610
- if (field.large) {
611
- col(`table.text('${name}')`);
612
- } else {
613
- col(`table.string('${name}', ${field.maxLength})`);
614
- }
615
- break;
616
- case 'DateTime':
617
- col(`table.timestamp('${name}')`);
618
- break;
619
- case 'ID':
620
- if (field.maxLength) {
621
- col(`table.string('${name}', ${get(field, 'maxLength')})`);
622
- } else {
623
- col(`table.uuid('${name}')`);
624
- }
625
- break;
626
- case 'Upload':
627
- break;
628
- default:
629
- throw new Error(`Unknown field type ${type}`);
630
- }
628
+ }
629
+ col();
630
+ break;
631
+ default:
632
+ throw new Error(`Unknown field type ${field.type}`);
631
633
  }
632
634
  }
633
635
  }
package/src/models.ts CHANGED
@@ -1,17 +1,12 @@
1
+ import { DateTime } from 'luxon';
2
+ import { Field } from '.';
1
3
  import type { Context } from './context';
2
4
  import type { OrderBy } from './resolvers/arguments';
3
- import type { Directive, Value } from './values';
5
+ import type { Value } from './values';
4
6
 
5
7
  export type RawModels = RawModel[];
6
8
 
7
- export type RawModel =
8
- | ScalarModel
9
- | EnumModel
10
- | RawEnumModel
11
- | InterfaceModel
12
- | ObjectModel
13
- | RawObjectModel
14
- | JsonObjectModel;
9
+ export type RawModel = ScalarModel | EnumModel | RawEnumModel | InterfaceModel | ObjectModel | RawObjectModel;
15
10
 
16
11
  type BaseModel = {
17
12
  name: string;
@@ -28,19 +23,11 @@ export type RawEnumModel = BaseModel & { type: 'raw-enum'; values: string[] };
28
23
  export type InterfaceModel = BaseModel & { type: 'interface'; fields: ModelField[] };
29
24
 
30
25
  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'>[];
26
+ type: 'raw';
27
+ fields: RawObjectField[];
40
28
  };
41
29
 
42
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- data is derived from the models
43
- export type Entity = Record<string, any>;
30
+ export type Entity = Record<string, unknown> & { createdAt?: DateTime; deletedAt?: DateTime };
44
31
 
45
32
  export type Action = 'create' | 'update' | 'delete' | 'restore';
46
33
 
@@ -55,13 +42,13 @@ export type MutationHook = (
55
42
  export type ObjectModel = BaseModel & {
56
43
  type: 'object';
57
44
  interfaces?: string[];
58
- // createdAt, createdBy, updatedAt, updatedBy can be null
59
- nonStrict?: boolean;
60
45
  queriable?: boolean;
61
46
  listQueriable?: boolean;
62
- creatable?: boolean;
63
- updatable?: boolean;
64
- deletable?: boolean;
47
+ creatable?: boolean | { createdBy?: Partial<RelationField>; createdAt?: Partial<DateTimeField> };
48
+ updatable?: boolean | { updatedBy?: Partial<RelationField>; updatedAt?: Partial<DateTimeField> };
49
+ deletable?:
50
+ | boolean
51
+ | { deleted?: Partial<BooleanField>; deletedBy?: Partial<RelationField>; deletedAt?: Partial<DateTimeField> };
65
52
  displayField?: string;
66
53
  defaultOrderBy?: OrderBy;
67
54
  fields: ModelField[];
@@ -71,136 +58,109 @@ export type ObjectModel = BaseModel & {
71
58
  oldName?: string;
72
59
  };
73
60
 
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';
61
+ type BaseNumberType = {
177
62
  unit?: 'million';
178
- intType?: 'currency';
179
63
  min?: number;
180
64
  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
65
  };
203
66
 
67
+ type BaseField = Omit<Field, 'type'>;
68
+
69
+ type PrimitiveField =
70
+ | { type: 'ID' }
71
+ | { type: 'Boolean' }
72
+ | {
73
+ type: 'String';
74
+ stringType?: 'email' | 'url' | 'phone';
75
+ large?: true;
76
+ maxLength?: number;
77
+ }
78
+ | {
79
+ type: 'DateTime';
80
+ dateTimeType?: 'year' | 'date' | 'datetime' | 'year_and_month';
81
+ endOfDay?: boolean;
82
+ }
83
+ | ({
84
+ type: 'Int';
85
+ intType?: 'currency';
86
+ } & BaseNumberType)
87
+ | ({
88
+ type: 'Float';
89
+ floatType?: 'currency' | 'percentage';
90
+ double?: boolean;
91
+ precision?: number;
92
+ scale?: number;
93
+ } & BaseNumberType)
94
+ | { type: 'Upload' };
95
+
96
+ type RawObjectField = BaseField & PrimitiveField;
97
+
98
+ export type ModelField = BaseField &
99
+ (
100
+ | PrimitiveField
101
+ | { type: 'json'; typeName: string }
102
+ | { type: 'enum'; typeName: string; possibleValues?: Value[] }
103
+ | { type: 'raw'; typeName: string }
104
+ | {
105
+ type: 'relation';
106
+ typeName: string;
107
+ toOne?: boolean;
108
+ reverse?: string;
109
+ foreignKey?: string;
110
+ onDelete?: 'cascade' | 'set-null';
111
+ }
112
+ ) & {
113
+ primary?: boolean;
114
+ unique?: boolean;
115
+ filterable?:
116
+ | boolean
117
+ | {
118
+ default?: Value;
119
+ };
120
+ searchable?: boolean;
121
+ orderable?: boolean;
122
+ comparable?: boolean;
123
+ queriable?:
124
+ | boolean
125
+ | {
126
+ roles?: string[];
127
+ };
128
+ creatable?:
129
+ | boolean
130
+ | {
131
+ roles?: string[];
132
+ };
133
+ updatable?:
134
+ | boolean
135
+ | {
136
+ roles?: string[];
137
+ };
138
+ generated?: boolean;
139
+ // The tooltip is "hidden" behind an icon in the admin forms
140
+ tooltip?: string;
141
+ defaultValue?: string | number | ReadonlyArray<string> | undefined;
142
+ // If true the field must be filled within forms but can be null in the database
143
+ required?: boolean;
144
+ indent?: boolean;
145
+ // If true the field is hidden in the admin interface
146
+ hidden?: boolean;
147
+
148
+ // temporary fields for the generation of migrations
149
+ deleted?: true;
150
+ oldName?: string;
151
+ };
152
+
153
+ export type IDField = Extract<ModelField, { type: 'ID' }>;
154
+ export type BooleanField = Extract<ModelField, { type: 'Boolean' }>;
155
+ export type StringField = Extract<ModelField, { type: 'String' }>;
156
+ export type DateTimeField = Extract<ModelField, { type: 'DateTime' }>;
157
+ export type IntField = Extract<ModelField, { type: 'Int' }>;
158
+ export type FloatField = Extract<ModelField, { type: 'Float' }>;
159
+ export type JsonField = Extract<ModelField, { type: 'json' }>;
160
+ export type EnumField = Extract<ModelField, { type: 'enum' }>;
161
+ export type RawField = Extract<ModelField, { type: 'raw' }>;
162
+ export type RelationField = Extract<ModelField, { type: 'relation' }>;
163
+
204
164
  export type Models = Model[];
205
165
 
206
166
  export type Model = ObjectModel & {
@@ -212,17 +172,13 @@ export type Model = ObjectModel & {
212
172
  };
213
173
 
214
174
  export type Relation = {
215
- field: ModelField;
175
+ field: RelationField;
216
176
  model: Model;
217
177
  reverseRelation: ReverseRelation;
218
178
  };
219
179
 
220
- export type ReverseRelation = {
221
- name: string;
222
- type: string;
223
- foreignKey: string;
224
- toOne: boolean;
180
+ export type ReverseRelation = RelationField & {
225
181
  model: Model;
226
- field: ModelField;
182
+ field: RelationField;
227
183
  fieldModel: Model;
228
184
  };
@@ -3,7 +3,7 @@ import { FullContext } from '../context';
3
3
  import { NotFoundError, PermissionError } from '../errors';
4
4
  import { Model } from '../models';
5
5
  import { AliasGenerator, hash, ors } from '../resolvers/utils';
6
- import { get, getModelPlural, summonByName } from '../utils';
6
+ import { get, getModelPlural, isRelation, summonByName } from '../utils';
7
7
  import { BasicValue } from '../values';
8
8
  import { PermissionAction, PermissionLink, PermissionStack } from './generate';
9
9
 
@@ -139,7 +139,7 @@ export const checkCanWrite = async (
139
139
  let linked = false;
140
140
 
141
141
  for (const field of model.fields
142
- .filter(({ relation }) => relation)
142
+ .filter(isRelation)
143
143
  .filter((field) => field.generated || (action === 'CREATE' ? field.creatable : field.updatable))) {
144
144
  const foreignKey = field.foreignKey || `${field.name}Id`;
145
145
  const foreignId = data[foreignKey] as string;
@@ -1,5 +1,5 @@
1
1
  import { Models } from '../models';
2
- import { summonByName } from '../utils';
2
+ import { isRelation, summonByName } from '../utils';
3
3
 
4
4
  export type PermissionAction = 'READ' | 'CREATE' | 'UPDATE' | 'DELETE' | 'RESTORE' | 'LINK';
5
5
 
@@ -114,7 +114,7 @@ const addPermissions = (models: Models, permissions: RolePermissions, links: Per
114
114
 
115
115
  if (block.RELATIONS) {
116
116
  for (const [relation, subBlock] of Object.entries(block.RELATIONS)) {
117
- const field = model.fields.find((field) => field.relation && field.name === relation);
117
+ const field = model.fields.filter(isRelation).find((field) => field.name === relation);
118
118
  let link: PermissionLink;
119
119
  if (field) {
120
120
  link = {
@@ -85,7 +85,7 @@ const applyWhere = (node: WhereNode, where: Where, ops: Ops<Knex.QueryBuilder>,
85
85
  const field = summonByName(node.model.fields, key);
86
86
  const fullKey = `${node.shortTableAlias}.${key}`;
87
87
 
88
- if (field.relation) {
88
+ if (field.type === 'relation') {
89
89
  const relation = get(node.model.relationsByName, field.name);
90
90
  const tableAlias = `${node.model.name}__W__${key}`;
91
91
  const subNode: WhereNode = {