@smartive/graphql-magic 15.4.0 → 16.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 (63) hide show
  1. package/.gqmrc.json +4 -2
  2. package/CHANGELOG.md +1 -6
  3. package/dist/bin/gqm.cjs +115 -42
  4. package/dist/cjs/index.cjs +111 -30
  5. package/dist/esm/api/execute.d.ts +1 -1
  6. package/dist/esm/context.d.ts +5 -4
  7. package/dist/esm/db/generate.d.ts +2 -1
  8. package/dist/esm/db/generate.js +13 -8
  9. package/dist/esm/db/generate.js.map +1 -1
  10. package/dist/esm/index.d.ts +1 -0
  11. package/dist/esm/index.js +1 -0
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/migrations/generate.d.ts +1 -0
  14. package/dist/esm/migrations/generate.js +28 -6
  15. package/dist/esm/migrations/generate.js.map +1 -1
  16. package/dist/esm/models/mutation-hook.d.ts +5 -12
  17. package/dist/esm/models/utils.d.ts +20 -7
  18. package/dist/esm/models/utils.js +8 -0
  19. package/dist/esm/models/utils.js.map +1 -1
  20. package/dist/esm/permissions/check.d.ts +2 -3
  21. package/dist/esm/permissions/check.js.map +1 -1
  22. package/dist/esm/resolvers/arguments.d.ts +1 -1
  23. package/dist/esm/resolvers/mutations.js +5 -2
  24. package/dist/esm/resolvers/mutations.js.map +1 -1
  25. package/dist/esm/schema/utils.js +19 -8
  26. package/dist/esm/schema/utils.js.map +1 -1
  27. package/dist/esm/utils/dates.d.ts +12 -0
  28. package/dist/esm/utils/dates.js +37 -0
  29. package/dist/esm/utils/dates.js.map +1 -0
  30. package/dist/esm/utils/index.d.ts +1 -0
  31. package/dist/esm/utils/index.js +3 -0
  32. package/dist/esm/utils/index.js.map +1 -0
  33. package/dist/esm/values.d.ts +1 -3
  34. package/docker-compose.yml +0 -1
  35. package/docs/docs/1-tutorial.md +6 -6
  36. package/docs/docs/6-graphql-server.md +1 -3
  37. package/docs/docs/7-graphql-client.md +1 -1
  38. package/docs/docs/8-permissions.md +145 -0
  39. package/docs/package-lock.json +177 -177
  40. package/docs/package.json +6 -6
  41. package/knexfile.ts +2 -2
  42. package/migrations/20230912185644_setup.ts +37 -8
  43. package/package.json +5 -4
  44. package/src/bin/gqm/codegen.ts +4 -3
  45. package/src/bin/gqm/gqm.ts +4 -2
  46. package/src/bin/gqm/settings.ts +37 -2
  47. package/src/bin/gqm/templates.ts +19 -8
  48. package/src/context.ts +9 -5
  49. package/src/db/generate.ts +15 -8
  50. package/src/index.ts +1 -0
  51. package/src/migrations/generate.ts +34 -16
  52. package/src/models/mutation-hook.ts +5 -8
  53. package/src/models/utils.ts +24 -0
  54. package/src/permissions/check.ts +2 -3
  55. package/src/resolvers/mutations.ts +10 -6
  56. package/src/schema/utils.ts +14 -2
  57. package/src/utils/dates.ts +48 -0
  58. package/src/utils/index.ts +3 -0
  59. package/src/values.ts +1 -5
  60. package/tests/generated/client/index.ts +3 -1
  61. package/tests/generated/db/index.ts +43 -43
  62. package/tests/utils/database/seed.ts +9 -5
  63. package/tests/utils/server.ts +3 -3
@@ -3,18 +3,18 @@ import { Knex } from 'knex';
3
3
  export const up = async (knex: Knex) => {
4
4
  await knex.raw(`CREATE TYPE "someEnum" AS ENUM ('A','B','C')`);
5
5
 
6
- await knex.raw(`CREATE TYPE "role" AS ENUM ('ADMIN','USER')`);
7
-
8
6
  await knex.raw(`CREATE TYPE "reactionType" AS ENUM ('Review','Question','Answer')`);
9
7
 
10
- await knex.schema.createTable('User', (table) => {
11
- table.uuid('id').notNullable().primary();
12
- table.string('username', undefined).nullable();
8
+ await knex.schema.alterTable('User', (table) => {
9
+ table.string('username', undefined);
10
+ });
11
+
12
+ await knex.schema.alterTable('User', (table) => {
13
13
  table.enum('role', null as any, {
14
14
  useNative: true,
15
15
  existingType: true,
16
16
  enumName: 'role',
17
- }).nullable();
17
+ }).nullable().alter();
18
18
  });
19
19
 
20
20
  await knex.schema.createTable('AnotherObject', (table) => {
@@ -101,9 +101,29 @@ export const up = async (knex: Knex) => {
101
101
  table.decimal('rating', undefined, undefined).nullable();
102
102
  });
103
103
 
104
+ await knex.schema.alterTable('User', (table) => {
105
+ table.dropColumn('createdAt');
106
+ table.dropColumn('updatedAt');
107
+ });
108
+
104
109
  };
105
110
 
106
111
  export const down = async (knex: Knex) => {
112
+ await knex.schema.alterTable('User', (table) => {
113
+ table.timestamp('createdAt');
114
+ table.timestamp('updatedAt');
115
+ });
116
+
117
+ await knex('User').update({
118
+ createdAt: 'TODO',
119
+ updatedAt: 'TODO',
120
+ });
121
+
122
+ await knex.schema.alterTable('User', (table) => {
123
+ table.timestamp('createdAt').notNullable().alter();
124
+ table.timestamp('updatedAt').notNullable().alter();
125
+ });
126
+
107
127
  await knex.schema.dropTable('ReviewRevision');
108
128
 
109
129
  await knex.schema.dropTable('Review');
@@ -118,10 +138,19 @@ export const down = async (knex: Knex) => {
118
138
 
119
139
  await knex.schema.dropTable('AnotherObject');
120
140
 
121
- await knex.schema.dropTable('User');
141
+ await knex.schema.alterTable('User', (table) => {
142
+ table.enum('role', null as any, {
143
+ useNative: true,
144
+ existingType: true,
145
+ enumName: 'role',
146
+ }).notNullable().alter();
147
+ });
148
+
149
+ await knex.schema.alterTable('User', (table) => {
150
+ table.dropColumn('username');
151
+ });
122
152
 
123
153
  await knex.raw('DROP TYPE "reactionType"');
124
- await knex.raw('DROP TYPE "role"');
125
154
  await knex.raw('DROP TYPE "someEnum"');
126
155
  };
127
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartive/graphql-magic",
3
- "version": "15.4.0",
3
+ "version": "16.0.0",
4
4
  "description": "",
5
5
  "source": "src/index.ts",
6
6
  "type": "module",
@@ -52,13 +52,14 @@
52
52
  "@graphql-codegen/typescript-resolvers": "^4.0.1",
53
53
  "code-block-writer": "^12.0.0",
54
54
  "commander": "^11.0.0",
55
+ "dayjs": "^1.11.10",
55
56
  "dotenv": "^16.3.1",
56
57
  "graphql": "^15.8.0",
57
58
  "inflection": "^2.0.1",
58
59
  "knex": "^3.0.1",
59
60
  "knex-schema-inspector": "^3.1.0",
60
61
  "lodash": "^4.17.21",
61
- "luxon": "^3.3.0",
62
+ "luxon": "^3.4.4",
62
63
  "pg": "^8.11.3",
63
64
  "simple-git": "^3.21.0",
64
65
  "ts-morph": "^19.0.0",
@@ -73,7 +74,7 @@
73
74
  "@types/jest": "29.5.12",
74
75
  "@types/lodash": "4.17.0",
75
76
  "@types/luxon": "3.4.2",
76
- "@types/pg": "8.11.4",
77
+ "@types/pg": "8.11.5",
77
78
  "@types/uuid": "9.0.8",
78
79
  "create-ts-index": "1.14.0",
79
80
  "del-cli": "5.1.0",
@@ -85,6 +86,6 @@
85
86
  "prettier": "2.8.8",
86
87
  "ts-jest": "29.1.2",
87
88
  "ts-node": "10.9.2",
88
- "typescript": "5.4.3"
89
+ "typescript": "5.4.5"
89
90
  }
90
91
  }
@@ -1,7 +1,8 @@
1
1
  import { generate } from '@graphql-codegen/cli';
2
+ import { DATE_CLASS, DATE_CLASS_IMPORT, DateLibrary } from '../../utils/dates';
2
3
  import { ensureDirectoryExists, getSetting } from './settings';
3
4
 
4
- export const generateGraphqlApiTypes = async () => {
5
+ export const generateGraphqlApiTypes = async (dateLibrary: DateLibrary) => {
5
6
  const generatedFolderPath = await getSetting('generatedFolderPath');
6
7
  await generate({
7
8
  overwrite: true,
@@ -9,12 +10,12 @@ export const generateGraphqlApiTypes = async () => {
9
10
  documents: undefined,
10
11
  generates: {
11
12
  [`${generatedFolderPath}/api/index.ts`]: {
12
- plugins: ['typescript', 'typescript-resolvers', { add: { content: `import { DateTime } from 'luxon';` } }],
13
+ plugins: ['typescript', 'typescript-resolvers', { add: { content: DATE_CLASS_IMPORT[dateLibrary] } }],
13
14
  },
14
15
  },
15
16
  config: {
16
17
  scalars: {
17
- DateTime: 'DateTime',
18
+ DateTime: DATE_CLASS[dateLibrary],
18
19
  },
19
20
  },
20
21
  });
@@ -12,6 +12,7 @@ import {
12
12
  getMigrationDate,
13
13
  printSchemaFromModels,
14
14
  } from '../..';
15
+ import { DateLibrary } from '../../utils/dates';
15
16
  import { generateGraphqlApiTypes, generateGraphqlClientTypes } from './codegen';
16
17
  import { parseKnexfile } from './parse-knexfile';
17
18
  import { parseModels } from './parse-models';
@@ -37,9 +38,10 @@ program
37
38
  const gqlModule = await getSetting('gqlModule');
38
39
  writeToFile(`${generatedFolderPath}/schema.graphql`, printSchemaFromModels(models));
39
40
  writeToFile(`${generatedFolderPath}/client/mutations.ts`, generateMutations(models, gqlModule));
40
- writeToFile(`${generatedFolderPath}/db/index.ts`, generateDBModels(models));
41
+ const dateLibrary = (await getSetting('dateLibrary')) as DateLibrary;
42
+ writeToFile(`${generatedFolderPath}/db/index.ts`, generateDBModels(models, dateLibrary));
41
43
  writeToFile(`${generatedFolderPath}/db/knex.ts`, generateKnexTables(models));
42
- await generateGraphqlApiTypes();
44
+ await generateGraphqlApiTypes(dateLibrary);
43
45
  await generateGraphqlClientTypes();
44
46
  });
45
47
 
@@ -1,7 +1,16 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { dirname } from 'path';
3
+ import { DateLibrary } from '../../utils/dates';
3
4
  import { readLine } from './readline';
4
- import { EMPTY_MODELS, EXECUTE, GET_ME, GITIGNORE, KNEXFILE } from './templates';
5
+ import {
6
+ EMPTY_MODELS,
7
+ EXECUTE,
8
+ GET_ME,
9
+ GITIGNORE,
10
+ KNEXFILE,
11
+ KNEXFILE_DAYJS_TYPE_PARSERS,
12
+ KNEXFILE_LUXON_TYPE_PARSERS,
13
+ } from './templates';
5
14
 
6
15
  const SETTINGS_PATH = '.gqmrc.json';
7
16
 
@@ -52,6 +61,32 @@ const DEFAULTS = {
52
61
  gqlModule: {
53
62
  defaultValue: '@smartive/graphql-magic',
54
63
  },
64
+ dateLibrary: {
65
+ question: 'Which date library to use (dayjs|luxon)?',
66
+ defaultValue: 'dayjs',
67
+ init: async (dateLibrary: DateLibrary) => {
68
+ const knexfilePath = await getSetting('knexfilePath');
69
+ switch (dateLibrary) {
70
+ case 'luxon': {
71
+ const timeZone = await getSetting('timeZone');
72
+ ensureFileContains(knexfilePath, 'luxon', KNEXFILE_LUXON_TYPE_PARSERS(timeZone));
73
+ break;
74
+ }
75
+ case 'dayjs':
76
+ ensureFileContains(knexfilePath, 'dayjs', KNEXFILE_DAYJS_TYPE_PARSERS);
77
+ break;
78
+ default:
79
+ throw new Error('Invalid or unsupported date library.');
80
+ }
81
+ },
82
+ },
83
+ timeZone: {
84
+ question: 'Which time zone to use?',
85
+ defaultValue: 'Europe/Zurich',
86
+ init: () => {
87
+ // Do nothing
88
+ },
89
+ },
55
90
  };
56
91
 
57
92
  type Settings = {
@@ -61,7 +96,7 @@ type Settings = {
61
96
  const initSetting = async (name: string) => {
62
97
  const { question, defaultValue, init } = DEFAULTS[name];
63
98
  const value = (await readLine(`${question} (${defaultValue})`)) || defaultValue;
64
- init(value);
99
+ await init(value);
65
100
  return value;
66
101
  };
67
102
 
@@ -17,14 +17,9 @@ const modelDefinitions: ModelDefinitions = [
17
17
  export const models = new Models(modelDefinitions);
18
18
  `;
19
19
 
20
- export const KNEXFILE = `import { DateTime } from 'luxon';
20
+ export const KNEXFILE = `
21
21
  import { types } from 'pg';
22
22
 
23
- const dateOids = { date: 1082, timestamptz: 1184, timestamp: 1114 };
24
- for (const oid of Object.values(dateOids)) {
25
- types.setTypeParser(oid, (val) => DateTime.fromSQL(val));
26
- }
27
-
28
23
  const numberOids = { int8: 20, float8: 701, numeric: 1700 };
29
24
  for (const oid of Object.values(numberOids)) {
30
25
  types.setTypeParser(oid, Number);
@@ -50,6 +45,24 @@ const knexConfig = {
50
45
  export default knexConfig;
51
46
  `;
52
47
 
48
+ export const KNEXFILE_LUXON_TYPE_PARSERS = (timeZone: string) => `
49
+ import { DateTime } from 'luxon';
50
+
51
+ const dateOids = { date: 1082, timestamptz: 1184, timestamp: 1114 };
52
+ for (const oid of Object.values(dateOids)) {
53
+ types.setTypeParser(oid, (val) => DateTime.fromSQL(val, { zone: "${timeZone}" }));
54
+ }
55
+ `;
56
+
57
+ export const KNEXFILE_DAYJS_TYPE_PARSERS = `
58
+ import { dayjs } from 'dayjs';
59
+
60
+ const dateOids = { date: 1082, timestamptz: 1184, timestamp: 1114 };
61
+ for (const oid of Object.values(dateOids)) {
62
+ types.setTypeParser(oid, (val) => dayjs(val));
63
+ }
64
+ `;
65
+
53
66
  export const GET_ME = `import { gql } from '@smartive/graphql-magic';
54
67
 
55
68
  export const GET_ME = gql\`
@@ -66,7 +79,6 @@ import knexConfig from "@/knexfile";
66
79
  import { Context, User, execute } from "@smartive/graphql-magic";
67
80
  import { randomUUID } from "crypto";
68
81
  import { knex } from 'knex';
69
- import { DateTime } from "luxon";
70
82
  import { models } from "../config/models";
71
83
 
72
84
  export const executeGraphql = async <T, V = undefined>(
@@ -89,7 +101,6 @@ export const executeGraphql = async <T, V = undefined>(
89
101
  user,
90
102
  models: models,
91
103
  permissions: { ADMIN: true, UNAUTHENTICATED: true },
92
- now: DateTime.local(),
93
104
  });
94
105
  await db.destroy();
95
106
 
package/src/context.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  import { DocumentNode, GraphQLResolveInfo } from 'graphql';
2
2
  import { IncomingMessage } from 'http';
3
3
  import { Knex } from 'knex';
4
- import { DateTime } from 'luxon';
5
4
  import { Models } from './models/models';
6
5
  import { Entity, MutationHook } from './models/mutation-hook';
7
6
  import { Permissions } from './permissions/generate';
8
7
  import { AliasGenerator } from './resolvers/utils';
8
+ import { AnyDateType } from './utils';
9
9
 
10
10
  // Minimal user structure required by graphql-magic
11
11
  export type User = { id: string; role: string };
12
12
 
13
- export type Context = {
13
+ export type Context<DateType extends AnyDateType = AnyDateType> = {
14
14
  req: IncomingMessage;
15
- now: DateTime;
15
+ now: DateType;
16
+ timeZone?: string;
16
17
  knex: Knex;
17
18
  document: DocumentNode;
18
19
  locale: string;
@@ -20,8 +21,11 @@ export type Context = {
20
21
  user?: User;
21
22
  models: Models;
22
23
  permissions: Permissions;
23
- mutationHook?: MutationHook;
24
+ mutationHook?: MutationHook<DateType>;
24
25
  handleUploads?: (data: Entity) => Promise<void>;
25
26
  };
26
27
 
27
- export type FullContext = Context & { info: GraphQLResolveInfo; aliases: AliasGenerator };
28
+ export type FullContext = Context & {
29
+ info: GraphQLResolveInfo;
30
+ aliases: AliasGenerator;
31
+ };
@@ -1,6 +1,7 @@
1
1
  import CodeBlockWriter from 'code-block-writer';
2
2
  import { EntityField, get, getColumnName, isCustomField, isInTable, isRootModel, not } from '..';
3
3
  import { Models } from '../models/models';
4
+ import { DATE_CLASS, DATE_CLASS_IMPORT, DateLibrary } from '../utils/dates';
4
5
 
5
6
  const PRIMITIVE_TYPES = {
6
7
  ID: 'string',
@@ -9,18 +10,17 @@ const PRIMITIVE_TYPES = {
9
10
  Int: 'number',
10
11
  Float: 'number',
11
12
  String: 'string',
12
- DateTime: 'DateTime | string',
13
13
  };
14
14
 
15
15
  const OPTIONAL_SEED_FIELDS = ['createdAt', 'createdById', 'updatedAt', 'updatedById', 'deletedAt', 'deletedById'];
16
16
 
17
- export const generateDBModels = (models: Models) => {
17
+ export const generateDBModels = (models: Models, dateLibrary: DateLibrary) => {
18
18
  const writer: CodeBlockWriter = new CodeBlockWriter['default']({
19
19
  useSingleQuote: true,
20
20
  indentNumberOfSpaces: 2,
21
21
  });
22
22
 
23
- writer.write(`import { DateTime } from 'luxon';`).blankLine();
23
+ writer.write(DATE_CLASS_IMPORT[dateLibrary]).blankLine();
24
24
 
25
25
  for (const enm of models.enums) {
26
26
  writer.write(`export type ${enm.name} = ${enm.values.map((v) => `'${v}'`).join(' | ')};`).blankLine();
@@ -36,7 +36,9 @@ export const generateDBModels = (models: Models) => {
36
36
  .write(`export type ${model.name} = `)
37
37
  .inlineBlock(() => {
38
38
  for (const field of fields.filter(not(isCustomField))) {
39
- writer.write(`'${getColumnName(field)}': ${getFieldType(field)}${field.nonNull ? '' : ' | null'};`).newLine();
39
+ writer
40
+ .write(`'${getColumnName(field)}': ${getFieldType(field, dateLibrary)}${field.nonNull ? '' : ' | null'};`)
41
+ .newLine();
40
42
  }
41
43
  })
42
44
  .blankLine();
@@ -48,7 +50,9 @@ export const generateDBModels = (models: Models) => {
48
50
  writer
49
51
  .write(
50
52
  `'${getColumnName(field)}'${field.nonNull && field.defaultValue === undefined ? '' : '?'}: ${getFieldType(
51
- field
53
+ field,
54
+ dateLibrary,
55
+ true
52
56
  )}${field.list ? ' | string' : ''}${field.nonNull ? '' : ' | null'};`
53
57
  )
54
58
  .newLine();
@@ -62,7 +66,7 @@ export const generateDBModels = (models: Models) => {
62
66
  for (const field of fields.filter(not(isCustomField)).filter(isInTable)) {
63
67
  writer
64
68
  .write(
65
- `'${getColumnName(field)}'?: ${getFieldType(field)}${field.list ? ' | string' : ''}${
69
+ `'${getColumnName(field)}'?: ${getFieldType(field, dateLibrary, true)}${field.list ? ' | string' : ''}${
66
70
  field.nonNull ? '' : ' | null'
67
71
  };`
68
72
  )
@@ -84,7 +88,7 @@ export const generateDBModels = (models: Models) => {
84
88
  .write(
85
89
  `'${getColumnName(field)}'${
86
90
  field.nonNull && field.defaultValue === undefined && !OPTIONAL_SEED_FIELDS.includes(fieldName) ? '' : '?'
87
- }: ${field.kind === 'enum' ? (field.list ? 'string[]' : 'string') : getFieldType(field)}${
91
+ }: ${field.kind === 'enum' ? (field.list ? 'string[]' : 'string') : getFieldType(field, dateLibrary, true)}${
88
92
  field.list ? ' | string' : ''
89
93
  }${field.nonNull ? '' : ' | null'};`
90
94
  )
@@ -104,7 +108,7 @@ export const generateDBModels = (models: Models) => {
104
108
  return writer.toString();
105
109
  };
106
110
 
107
- const getFieldType = (field: EntityField) => {
111
+ const getFieldType = (field: EntityField, dateLibrary: DateLibrary, input?: boolean) => {
108
112
  const kind = field.kind;
109
113
  switch (kind) {
110
114
  case 'json':
@@ -119,6 +123,9 @@ const getFieldType = (field: EntityField) => {
119
123
  throw new Error(`Custom fields are not in the db.`);
120
124
  case 'primitive':
121
125
  case undefined:
126
+ if (field.type === 'DateTime') {
127
+ return (input ? `(${DATE_CLASS[dateLibrary]} | string)` : DATE_CLASS[dateLibrary]) + (field.list ? '[]' : '');
128
+ }
122
129
  return get(PRIMITIVE_TYPES, field.type) + (field.list ? '[]' : '');
123
130
  default: {
124
131
  const exhaustiveCheck: never = kind;
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export * from './models';
8
8
  export * from './permissions';
9
9
  export * from './resolvers';
10
10
  export * from './schema';
11
+ export * from './utils';
11
12
  export * from './context';
12
13
  export * from './errors';
13
14
  export * from './values';
@@ -8,6 +8,7 @@ import { EntityField, EntityModel, EnumModel, Models } from '../models/models';
8
8
  import {
9
9
  and,
10
10
  get,
11
+ isCreatableModel,
11
12
  isInherited,
12
13
  isUpdatableField,
13
14
  isUpdatableModel,
@@ -147,9 +148,7 @@ export class MigrationGenerator {
147
148
  .filter(
148
149
  ({ name, ...field }) =>
149
150
  field.kind !== 'custom' &&
150
- !this.columns[model.name].some(
151
- (col) => col.name === (field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
152
- )
151
+ !this.getColumn(model.name, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
153
152
  ),
154
153
  up,
155
154
  down
@@ -157,7 +156,7 @@ export class MigrationGenerator {
157
156
 
158
157
  // Update fields
159
158
  const existingFields = model.fields.filter(({ name, kind, nonNull }) => {
160
- const col = this.columns[model.name].find((col) => col.name === (kind === 'relation' ? `${name}Id` : name));
159
+ const col = this.getColumn(model.name, kind === 'relation' ? `${name}Id` : name);
161
160
  if (!col) {
162
161
  return false;
163
162
  }
@@ -216,9 +215,7 @@ export class MigrationGenerator {
216
215
  .filter(
217
216
  ({ name, ...field }) =>
218
217
  field.kind !== 'custom' &&
219
- !this.columns[revisionTable].some(
220
- (col) => col.name === (field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
221
- )
218
+ !this.getColumn(revisionTable, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
222
219
  );
223
220
 
224
221
  this.createRevisionFields(model, missingRevisionFields, up, down);
@@ -229,9 +226,7 @@ export class MigrationGenerator {
229
226
  field.kind !== 'custom' &&
230
227
  !updatable &&
231
228
  !(field.kind === 'relation' && field.foreignKey === 'id') &&
232
- this.columns[revisionTable].some(
233
- (col) => col.name === (field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
234
- )
229
+ this.getColumn(revisionTable, field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
235
230
  );
236
231
  this.createRevisionFields(model, revisionFieldsToRemove, down, up);
237
232
  }
@@ -241,12 +236,31 @@ export class MigrationGenerator {
241
236
 
242
237
  for (const model of models.entities) {
243
238
  if (tables.includes(model.name)) {
244
- this.createFields(
245
- model,
246
- model.fields.filter(({ name, deleted }) => deleted && this.columns[model.name].some((col) => col.name === name)),
247
- down,
248
- up
249
- );
239
+ const fieldsToDelete = model.fields.filter(({ name, deleted }) => deleted && this.getColumn(model.name, name));
240
+
241
+ if (!isCreatableModel(model)) {
242
+ if (this.getColumn(model.name, 'createdAt')) {
243
+ fieldsToDelete.push({ name: 'createdAt', type: 'DateTime', nonNull: true });
244
+ }
245
+
246
+ if (this.getColumn(model.name, 'createdBy')) {
247
+ fieldsToDelete.push({ name: 'createdBy', kind: 'relation', type: 'User', nonNull: true });
248
+ }
249
+ }
250
+
251
+ if (!isUpdatableModel(model)) {
252
+ if (this.getColumn(model.name, 'updatedAt')) {
253
+ fieldsToDelete.push({ name: 'updatedAt', type: 'DateTime', nonNull: true });
254
+ }
255
+
256
+ if (this.getColumn(model.name, 'updatedBy')) {
257
+ fieldsToDelete.push({ name: 'updatedBy', kind: 'relation', type: 'User', nonNull: true });
258
+ }
259
+ }
260
+
261
+ if (fieldsToDelete.length) {
262
+ this.createFields(model, fieldsToDelete, down, up);
263
+ }
250
264
 
251
265
  if (isUpdatableModel(model)) {
252
266
  this.createRevisionFields(
@@ -664,6 +678,10 @@ export class MigrationGenerator {
664
678
  }
665
679
  }
666
680
  }
681
+
682
+ private getColumn(tableName: string, columnName: string) {
683
+ return this.columns[tableName].find((col) => col.name === columnName);
684
+ }
667
685
  }
668
686
 
669
687
  export const getMigrationDate = () => {
@@ -1,17 +1,14 @@
1
- import { DateTime } from 'luxon';
2
- import { Context } from '..';
1
+ import { AnyDateType, Context } from '..';
3
2
  import { EntityModel } from './models';
4
3
 
5
- export type Entity = Record<string, unknown> & { createdAt?: DateTime; deletedAt?: DateTime };
6
-
7
- export type FullEntity = Entity & { id: string };
4
+ export type Entity = Record<string, unknown>;
8
5
 
9
6
  export type Action = 'create' | 'update' | 'delete' | 'restore';
10
7
 
11
- export type MutationHook = (
8
+ export type MutationHook<DateType extends AnyDateType = AnyDateType> = (
12
9
  model: EntityModel,
13
10
  action: Action,
14
11
  when: 'before' | 'after',
15
- data: { prev: Entity; input: Entity; normalizedInput: Entity; next: FullEntity },
16
- ctx: Context
12
+ data: { prev: Entity; input: Entity; normalizedInput: Entity; next: Entity },
13
+ ctx: Context<DateType>
17
14
  ) => Promise<void>;
@@ -58,8 +58,12 @@ export const isInputModel = (model: Model): model is InputModel => model instanc
58
58
 
59
59
  export const isInterfaceModel = (model: Model): model is InterfaceModel => model instanceof InterfaceModel;
60
60
 
61
+ export const isCreatableModel = (model: EntityModel) => model.creatable && model.fields.some(isCreatableField);
62
+
61
63
  export const isUpdatableModel = (model: EntityModel) => model.updatable && model.fields.some(isUpdatableField);
62
64
 
65
+ export const isCreatableField = (field: EntityField) => !field.inherited && !!field.creatable;
66
+
63
67
  export const isUpdatableField = (field: EntityField) => !field.inherited && !!field.updatable;
64
68
 
65
69
  export const modelNeedsTable = (model: EntityModel) => model.fields.some((field) => !field.inherited);
@@ -169,3 +173,23 @@ export const retry = async <T>(cb: () => Promise<T>, condition: (e: any) => bool
169
173
  }
170
174
  }
171
175
  };
176
+
177
+ type Typeof = {
178
+ string: string;
179
+ number: number;
180
+ bigint: bigint;
181
+ boolean: boolean;
182
+ symbol: symbol;
183
+ undefined: undefined;
184
+ object: object;
185
+ // eslint-disable-next-line @typescript-eslint/ban-types
186
+ function: Function;
187
+ };
188
+
189
+ export const as = <T extends keyof Typeof>(value: unknown, type: T): Typeof[T] => {
190
+ if (typeof value !== type) {
191
+ throw new Error(`No string`);
192
+ }
193
+
194
+ return value as Typeof[T];
195
+ };
@@ -4,7 +4,6 @@ import { NotFoundError, PermissionError } from '../errors';
4
4
  import { EntityModel } from '../models/models';
5
5
  import { get, isRelation } from '../models/utils';
6
6
  import { AliasGenerator, hash, ors } from '../resolvers/utils';
7
- import { BasicValue } from '../values';
8
7
  import { PermissionAction, PermissionLink, PermissionStack } from './generate';
9
8
 
10
9
  export const getRole = (ctx: Pick<FullContext, 'user'>) => ctx.user?.role ?? 'UNAUTHENTICATED';
@@ -89,7 +88,7 @@ export const applyPermissions = (
89
88
  export const getEntityToMutate = async (
90
89
  ctx: Pick<FullContext, 'models' | 'permissions' | 'user' | 'knex'>,
91
90
  model: EntityModel,
92
- where: Record<string, BasicValue>,
91
+ where: Record<string, unknown>,
93
92
  action: 'UPDATE' | 'DELETE' | 'RESTORE'
94
93
  ) => {
95
94
  const query = ctx
@@ -132,7 +131,7 @@ export const getEntityToMutate = async (
132
131
  export const checkCanWrite = async (
133
132
  ctx: Pick<FullContext, 'models' | 'permissions' | 'user' | 'knex'>,
134
133
  model: EntityModel,
135
- data: Record<string, BasicValue>,
134
+ data: Record<string, unknown>,
136
135
  action: 'CREATE' | 'UPDATE'
137
136
  ) => {
138
137
  const permissionStack = getPermissionStack(ctx, model.name, action);
@@ -1,12 +1,12 @@
1
1
  import { GraphQLResolveInfo } from 'graphql';
2
- import { DateTime } from 'luxon';
3
2
  import { v4 as uuid } from 'uuid';
4
3
  import { Context, FullContext } from '../context';
5
4
  import { ForbiddenError, GraphQLError } from '../errors';
6
5
  import { EntityField, EntityModel } from '../models/models';
7
- import { Entity, FullEntity } from '../models/mutation-hook';
6
+ import { Entity } from '../models/mutation-hook';
8
7
  import { get, isPrimitive, it, typeToField } from '../models/utils';
9
8
  import { applyPermissions, checkCanWrite, getEntityToMutate } from '../permissions/check';
9
+ import { anyDateToLuxon } from '../utils';
10
10
  import { resolve } from './resolver';
11
11
  import { AliasGenerator } from './utils';
12
12
 
@@ -151,7 +151,7 @@ const del = async (model: EntityModel, { where, dryRun }: { where: any; dryRun:
151
151
  const mutations: Callbacks = [];
152
152
  const afterHooks: Callbacks = [];
153
153
 
154
- const deleteCascade = async (currentModel: EntityModel, entity: FullEntity) => {
154
+ const deleteCascade = async (currentModel: EntityModel, entity: Entity) => {
155
155
  if (entity.deleted) {
156
156
  return;
157
157
  }
@@ -266,8 +266,12 @@ const restore = async (model: EntityModel, { where }: { where: any }, ctx: FullC
266
266
  const mutations: Callbacks = [];
267
267
  const afterHooks: Callbacks = [];
268
268
 
269
- const restoreCascade = async (currentModel: EntityModel, relatedEntity: FullEntity) => {
270
- if (!relatedEntity.deleted || !relatedEntity.deletedAt || !relatedEntity.deletedAt.equals(entity.deletedAt)) {
269
+ const restoreCascade = async (currentModel: EntityModel, relatedEntity: Entity) => {
270
+ if (
271
+ !relatedEntity.deleted ||
272
+ !relatedEntity.deletedAt ||
273
+ anyDateToLuxon(relatedEntity.deletedAt, ctx.timeZone).equals(anyDateToLuxon(entity.deletedAt, ctx.timeZone))
274
+ ) {
271
275
  return;
272
276
  }
273
277
 
@@ -365,7 +369,7 @@ const sanitize = (ctx: FullContext, model: EntityModel, data: Entity) => {
365
369
  }
366
370
 
367
371
  if (isEndOfDay(field) && data[key]) {
368
- data[key] = (data[key] as DateTime).endOf('day');
372
+ data[key] = anyDateToLuxon(data[key], ctx.timeZone);
369
373
  continue;
370
374
  }
371
375