@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.
- package/.gqmrc.json +4 -2
- package/CHANGELOG.md +1 -6
- package/dist/bin/gqm.cjs +115 -42
- package/dist/cjs/index.cjs +111 -30
- package/dist/esm/api/execute.d.ts +1 -1
- package/dist/esm/context.d.ts +5 -4
- package/dist/esm/db/generate.d.ts +2 -1
- package/dist/esm/db/generate.js +13 -8
- package/dist/esm/db/generate.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/migrations/generate.d.ts +1 -0
- package/dist/esm/migrations/generate.js +28 -6
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models/mutation-hook.d.ts +5 -12
- package/dist/esm/models/utils.d.ts +20 -7
- package/dist/esm/models/utils.js +8 -0
- package/dist/esm/models/utils.js.map +1 -1
- package/dist/esm/permissions/check.d.ts +2 -3
- package/dist/esm/permissions/check.js.map +1 -1
- package/dist/esm/resolvers/arguments.d.ts +1 -1
- package/dist/esm/resolvers/mutations.js +5 -2
- package/dist/esm/resolvers/mutations.js.map +1 -1
- package/dist/esm/schema/utils.js +19 -8
- package/dist/esm/schema/utils.js.map +1 -1
- package/dist/esm/utils/dates.d.ts +12 -0
- package/dist/esm/utils/dates.js +37 -0
- package/dist/esm/utils/dates.js.map +1 -0
- package/dist/esm/utils/index.d.ts +1 -0
- package/dist/esm/utils/index.js +3 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/esm/values.d.ts +1 -3
- package/docker-compose.yml +0 -1
- package/docs/docs/1-tutorial.md +6 -6
- package/docs/docs/6-graphql-server.md +1 -3
- package/docs/docs/7-graphql-client.md +1 -1
- package/docs/docs/8-permissions.md +145 -0
- package/docs/package-lock.json +177 -177
- package/docs/package.json +6 -6
- package/knexfile.ts +2 -2
- package/migrations/20230912185644_setup.ts +37 -8
- package/package.json +5 -4
- package/src/bin/gqm/codegen.ts +4 -3
- package/src/bin/gqm/gqm.ts +4 -2
- package/src/bin/gqm/settings.ts +37 -2
- package/src/bin/gqm/templates.ts +19 -8
- package/src/context.ts +9 -5
- package/src/db/generate.ts +15 -8
- package/src/index.ts +1 -0
- package/src/migrations/generate.ts +34 -16
- package/src/models/mutation-hook.ts +5 -8
- package/src/models/utils.ts +24 -0
- package/src/permissions/check.ts +2 -3
- package/src/resolvers/mutations.ts +10 -6
- package/src/schema/utils.ts +14 -2
- package/src/utils/dates.ts +48 -0
- package/src/utils/index.ts +3 -0
- package/src/values.ts +1 -5
- package/tests/generated/client/index.ts +3 -1
- package/tests/generated/db/index.ts +43 -43
- package/tests/utils/database/seed.ts +9 -5
- 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.
|
|
11
|
-
table.
|
|
12
|
-
|
|
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.
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
89
|
+
"typescript": "5.4.5"
|
|
89
90
|
}
|
|
90
91
|
}
|
package/src/bin/gqm/codegen.ts
CHANGED
|
@@ -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:
|
|
13
|
+
plugins: ['typescript', 'typescript-resolvers', { add: { content: DATE_CLASS_IMPORT[dateLibrary] } }],
|
|
13
14
|
},
|
|
14
15
|
},
|
|
15
16
|
config: {
|
|
16
17
|
scalars: {
|
|
17
|
-
DateTime:
|
|
18
|
+
DateTime: DATE_CLASS[dateLibrary],
|
|
18
19
|
},
|
|
19
20
|
},
|
|
20
21
|
});
|
package/src/bin/gqm/gqm.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/bin/gqm/settings.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/bin/gqm/templates.ts
CHANGED
|
@@ -17,14 +17,9 @@ const modelDefinitions: ModelDefinitions = [
|
|
|
17
17
|
export const models = new Models(modelDefinitions);
|
|
18
18
|
`;
|
|
19
19
|
|
|
20
|
-
export const KNEXFILE = `
|
|
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:
|
|
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 & {
|
|
28
|
+
export type FullContext = Context & {
|
|
29
|
+
info: GraphQLResolveInfo;
|
|
30
|
+
aliases: AliasGenerator;
|
|
31
|
+
};
|
package/src/db/generate.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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 @@ 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 {
|
|
2
|
-
import { Context } from '..';
|
|
1
|
+
import { AnyDateType, Context } from '..';
|
|
3
2
|
import { EntityModel } from './models';
|
|
4
3
|
|
|
5
|
-
export type Entity = Record<string, unknown
|
|
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:
|
|
16
|
-
ctx: Context
|
|
12
|
+
data: { prev: Entity; input: Entity; normalizedInput: Entity; next: Entity },
|
|
13
|
+
ctx: Context<DateType>
|
|
17
14
|
) => Promise<void>;
|
package/src/models/utils.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/permissions/check.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
270
|
-
if (
|
|
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]
|
|
372
|
+
data[key] = anyDateToLuxon(data[key], ctx.timeZone);
|
|
369
373
|
continue;
|
|
370
374
|
}
|
|
371
375
|
|