@smartive/graphql-magic 7.0.1 → 8.1.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/CHANGELOG.md +3 -3
- package/README.md +8 -15
- package/dist/bin/gqm.cjs +1812 -0
- package/dist/cjs/index.cjs +568 -30
- package/dist/esm/bin/gqm.d.ts +2 -0
- package/dist/esm/bin/gqm.js +121 -0
- package/dist/esm/bin/gqm.js.map +1 -0
- package/dist/esm/client/mutations.d.ts +2 -2
- package/dist/esm/client/mutations.js +2 -1
- package/dist/esm/client/mutations.js.map +1 -1
- package/dist/esm/db/generate.js +7 -7
- package/dist/esm/db/generate.js.map +1 -1
- package/dist/esm/gqm/codegen.d.ts +2 -0
- package/dist/esm/gqm/codegen.js +46 -0
- package/dist/esm/gqm/codegen.js.map +1 -0
- package/dist/esm/gqm/index.d.ts +9 -0
- package/dist/esm/gqm/index.js +11 -0
- package/dist/esm/gqm/index.js.map +1 -0
- package/dist/esm/gqm/parse-knexfile.d.ts +2 -0
- package/dist/esm/gqm/parse-knexfile.js +19 -0
- package/dist/esm/gqm/parse-knexfile.js.map +1 -0
- package/dist/esm/gqm/parse-models.d.ts +2 -0
- package/dist/esm/gqm/parse-models.js +19 -0
- package/dist/esm/gqm/parse-models.js.map +1 -0
- package/dist/esm/gqm/readline.d.ts +1 -0
- package/dist/esm/gqm/readline.js +14 -0
- package/dist/esm/gqm/readline.js.map +1 -0
- package/dist/esm/gqm/settings.d.ts +9 -0
- package/dist/esm/gqm/settings.js +98 -0
- package/dist/esm/gqm/settings.js.map +1 -0
- package/dist/esm/gqm/static-eval.d.ts +3 -0
- package/dist/esm/gqm/static-eval.js +188 -0
- package/dist/esm/gqm/static-eval.js.map +1 -0
- package/dist/esm/gqm/templates.d.ts +4 -0
- package/dist/esm/gqm/templates.js +62 -0
- package/dist/esm/gqm/templates.js.map +1 -0
- package/dist/esm/gqm/utils.d.ts +2 -0
- package/dist/esm/gqm/utils.js +22 -0
- package/dist/esm/gqm/utils.js.map +1 -0
- package/dist/esm/gqm/visitor.d.ts +8 -0
- package/dist/esm/gqm/visitor.js +15 -0
- package/dist/esm/gqm/visitor.js.map +1 -0
- 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 +16 -5
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models/models.d.ts +16 -10
- package/dist/esm/models/utils.d.ts +17 -9
- package/dist/esm/models/utils.js +6 -5
- package/dist/esm/models/utils.js.map +1 -1
- package/dist/esm/resolvers/node.js +2 -2
- package/dist/esm/resolvers/node.js.map +1 -1
- package/dist/esm/resolvers/resolver.js +1 -1
- package/dist/esm/resolvers/resolver.js.map +1 -1
- package/dist/esm/schema/generate.js +16 -4
- package/dist/esm/schema/generate.js.map +1 -1
- package/package.json +18 -5
- package/src/bin/gqm.ts +146 -0
- package/src/client/mutations.ts +4 -3
- package/src/db/generate.ts +7 -7
- package/src/gqm/codegen.ts +47 -0
- package/src/gqm/index.ts +11 -0
- package/src/gqm/parse-knexfile.ts +21 -0
- package/src/gqm/parse-models.ts +24 -0
- package/src/gqm/readline.ts +15 -0
- package/src/gqm/settings.ts +112 -0
- package/src/gqm/static-eval.ts +203 -0
- package/src/gqm/templates.ts +64 -0
- package/src/gqm/utils.ts +23 -0
- package/src/gqm/visitor.ts +29 -0
- package/src/index.ts +1 -0
- package/src/migrations/generate.ts +18 -5
- package/src/models/models.ts +12 -7
- package/src/models/utils.ts +11 -8
- package/src/resolvers/node.ts +2 -2
- package/src/resolvers/resolver.ts +1 -1
- package/src/schema/generate.ts +17 -4
- package/tests/utils/generate-migration.ts +2 -13
- package/tests/utils/models.ts +4 -4
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { generate } from '@graphql-codegen/cli';
|
|
2
|
+
import { getSetting } from './settings';
|
|
3
|
+
|
|
4
|
+
export const generateGraphqlApiTypes = async () => {
|
|
5
|
+
const generatedFolderPath = await getSetting('generatedFolderPath');
|
|
6
|
+
await generate({
|
|
7
|
+
overwrite: true,
|
|
8
|
+
schema: `${generatedFolderPath}/schema.graphql`,
|
|
9
|
+
documents: null,
|
|
10
|
+
generates: {
|
|
11
|
+
[`${generatedFolderPath}/api/index.ts`]: {
|
|
12
|
+
plugins: ['typescript', 'typescript-resolvers', { add: { content: `import { DateTime } from 'luxon';` } }],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
config: {
|
|
16
|
+
scalars: {
|
|
17
|
+
DateTime: 'DateTime',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const generateGraphqlClientTypes = async () => {
|
|
24
|
+
const generatedFolderPath = await getSetting('generatedFolderPath');
|
|
25
|
+
await generate({
|
|
26
|
+
schema: `${generatedFolderPath}/schema.graphql`,
|
|
27
|
+
documents: ['./src/**/*.ts', './src/**/*.tsx'],
|
|
28
|
+
generates: {
|
|
29
|
+
[`${generatedFolderPath}/client/index.ts`]: {
|
|
30
|
+
plugins: ['typescript', 'typescript-operations', 'typescript-compatibility'],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
config: {
|
|
34
|
+
preResolveTypes: true, // Simplifies the generated types
|
|
35
|
+
namingConvention: 'keep', // Keeps naming as-is
|
|
36
|
+
nonOptionalTypename: true, // Forces `__typename` on all selection sets
|
|
37
|
+
skipTypeNameForRoot: true, // Don't generate __typename for root types
|
|
38
|
+
avoidOptionals: {
|
|
39
|
+
// Avoids optionals on the level of the field
|
|
40
|
+
field: true,
|
|
41
|
+
},
|
|
42
|
+
scalars: {
|
|
43
|
+
DateTime: 'string',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
};
|
package/src/gqm/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// created from 'create-ts-index'
|
|
2
|
+
|
|
3
|
+
export * from './codegen';
|
|
4
|
+
export * from './parse-knexfile';
|
|
5
|
+
export * from './parse-models';
|
|
6
|
+
export * from './readline';
|
|
7
|
+
export * from './settings';
|
|
8
|
+
export * from './static-eval';
|
|
9
|
+
export * from './templates';
|
|
10
|
+
export * from './utils';
|
|
11
|
+
export * from './visitor';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IndentationText, Project } from 'ts-morph';
|
|
2
|
+
import { ensureFileExists } from './settings';
|
|
3
|
+
import { staticEval } from './static-eval';
|
|
4
|
+
import { KNEXFILE } from './templates';
|
|
5
|
+
import { findDeclarationInFile } from './utils';
|
|
6
|
+
|
|
7
|
+
export const KNEXFILE_PATH = `knexfile.ts`;
|
|
8
|
+
|
|
9
|
+
export const parseKnexfile = async () => {
|
|
10
|
+
const project = new Project({
|
|
11
|
+
manipulationSettings: {
|
|
12
|
+
indentationText: IndentationText.TwoSpaces,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
ensureFileExists(KNEXFILE_PATH, KNEXFILE);
|
|
16
|
+
|
|
17
|
+
const sourceFile = project.addSourceFileAtPath(KNEXFILE_PATH);
|
|
18
|
+
const configDeclaration = findDeclarationInFile(sourceFile, 'config');
|
|
19
|
+
const config = staticEval(configDeclaration, {});
|
|
20
|
+
return config;
|
|
21
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IndentationText, Project } from 'ts-morph';
|
|
2
|
+
import { RawModels } from '..';
|
|
3
|
+
import { getSetting, writeToFile } from './settings';
|
|
4
|
+
import { staticEval } from './static-eval';
|
|
5
|
+
import { findDeclarationInFile } from './utils';
|
|
6
|
+
|
|
7
|
+
export const parseModels = async () => {
|
|
8
|
+
const project = new Project({
|
|
9
|
+
manipulationSettings: {
|
|
10
|
+
indentationText: IndentationText.TwoSpaces,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
const modelsPath = await getSetting('modelsPath');
|
|
14
|
+
const sourceFile = project.addSourceFileAtPath(modelsPath);
|
|
15
|
+
|
|
16
|
+
const modelsDeclaration = findDeclarationInFile(sourceFile, 'rawModels');
|
|
17
|
+
|
|
18
|
+
const rawModels = staticEval(modelsDeclaration, {});
|
|
19
|
+
|
|
20
|
+
const generatedFolderPath = await getSetting('generatedFolderPath');
|
|
21
|
+
writeToFile(`${generatedFolderPath}/models.json`, JSON.stringify(rawModels, null, 2));
|
|
22
|
+
|
|
23
|
+
return rawModels as RawModels;
|
|
24
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
|
|
3
|
+
export const readLine = (prompt: string): Promise<string> => {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return new Promise<string>((resolve) => {
|
|
10
|
+
rl.question(prompt, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { readLine } from './readline';
|
|
4
|
+
import { CLIENT_CODEGEN, EMPTY_MODELS, GRAPHQL_CODEGEN } from './templates';
|
|
5
|
+
|
|
6
|
+
const SETTINGS_PATH = '.gqmrc.json';
|
|
7
|
+
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
modelsPath: {
|
|
10
|
+
question: 'What is the models path?',
|
|
11
|
+
defaultValue: 'src/config/models.ts',
|
|
12
|
+
init: (path: string) => {
|
|
13
|
+
ensureFileExists(path, EMPTY_MODELS);
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
generatedFolderPath: {
|
|
17
|
+
question: 'What is the path for generated stuff?',
|
|
18
|
+
defaultValue: 'src/generated',
|
|
19
|
+
init: (path: string) => {
|
|
20
|
+
ensureFileExists(`${path}/.gitkeep`, '');
|
|
21
|
+
ensureFileExists(`${path}/db/.gitkeep`, '');
|
|
22
|
+
ensureFileExists(`${path}/api/.gitkeep`, '');
|
|
23
|
+
ensureFileExists(`${path}/client/.gitkeep`, '');
|
|
24
|
+
ensureFileExists(`graphql-codegen.yml`, GRAPHQL_CODEGEN(path));
|
|
25
|
+
ensureFileExists(`client-codegen.yml`, CLIENT_CODEGEN(path));
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type Settings = {
|
|
31
|
+
modelsPath: string;
|
|
32
|
+
generatedFolderPath: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const initSetting = async (name: string) => {
|
|
36
|
+
const { question, defaultValue, init } = DEFAULTS[name];
|
|
37
|
+
const value = (await readLine(`${question} (${defaultValue})`)) || defaultValue;
|
|
38
|
+
init(value);
|
|
39
|
+
return value;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const initSettings = async () => {
|
|
43
|
+
const settings: Settings = {} as Settings;
|
|
44
|
+
for (const name of Object.keys(DEFAULTS)) {
|
|
45
|
+
settings[name] = await initSetting(name);
|
|
46
|
+
}
|
|
47
|
+
saveSettings(settings);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const saveSettings = (settings: Settings) => {
|
|
51
|
+
writeToFile(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getSettings = async (): Promise<Settings> => {
|
|
55
|
+
if (!existsSync(SETTINGS_PATH)) {
|
|
56
|
+
await initSettings();
|
|
57
|
+
}
|
|
58
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getSetting = async (name: keyof Settings): Promise<string> => {
|
|
62
|
+
const settings = await getSettings();
|
|
63
|
+
if (!(name in settings)) {
|
|
64
|
+
settings[name] = await initSetting(name);
|
|
65
|
+
saveSettings(settings);
|
|
66
|
+
}
|
|
67
|
+
return settings[name];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const ensureDirectoryExists = (filePath: string) => {
|
|
71
|
+
const dir = dirname(filePath);
|
|
72
|
+
|
|
73
|
+
if (existsSync(dir)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
ensureDirectoryExists(dir);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
mkdirSync(dir);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err.code === 'EEXIST') {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const ensureFileExists = (filePath: string, content: string) => {
|
|
91
|
+
if (!existsSync(filePath)) {
|
|
92
|
+
console.info(`Creating ${filePath}`);
|
|
93
|
+
ensureDirectoryExists(filePath);
|
|
94
|
+
writeFileSync(filePath, content);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const writeToFile = (filePath: string, content: string) => {
|
|
99
|
+
ensureDirectoryExists(filePath);
|
|
100
|
+
if (existsSync(filePath)) {
|
|
101
|
+
const currentContent = readFileSync(filePath, 'utf-8');
|
|
102
|
+
if (content === currentContent) {
|
|
103
|
+
// console.info(`${filePath} unchanged`);
|
|
104
|
+
} else {
|
|
105
|
+
writeFileSync(filePath, content);
|
|
106
|
+
console.info(`${filePath} updated`);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
writeFileSync(filePath, content);
|
|
110
|
+
console.info(`Created ${filePath}`);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Dictionary } from 'lodash';
|
|
2
|
+
import {
|
|
3
|
+
CaseClause,
|
|
4
|
+
ElementAccessExpression,
|
|
5
|
+
Identifier,
|
|
6
|
+
Node,
|
|
7
|
+
ObjectLiteralExpression,
|
|
8
|
+
PrefixUnaryExpression,
|
|
9
|
+
ShorthandPropertyAssignment,
|
|
10
|
+
SyntaxKind,
|
|
11
|
+
TemplateExpression,
|
|
12
|
+
TemplateTail,
|
|
13
|
+
} from 'ts-morph';
|
|
14
|
+
import { Visitor, visit } from './visitor';
|
|
15
|
+
|
|
16
|
+
export const staticEval = (node: Node, context: Dictionary<unknown>) =>
|
|
17
|
+
visit<unknown, Dictionary<unknown>>(node, context, visitor);
|
|
18
|
+
|
|
19
|
+
const visitor: Visitor<unknown, Dictionary<unknown>> = {
|
|
20
|
+
undefined: () => undefined,
|
|
21
|
+
[SyntaxKind.VariableDeclaration]: (node, context) => staticEval(node.getInitializer(), context),
|
|
22
|
+
[SyntaxKind.ArrayLiteralExpression]: (node, context) => {
|
|
23
|
+
const values: unknown[] = [];
|
|
24
|
+
for (const value of node.getElements()) {
|
|
25
|
+
if (value.isKind(SyntaxKind.SpreadElement)) {
|
|
26
|
+
values.push(...(staticEval(value, context) as unknown[]));
|
|
27
|
+
} else {
|
|
28
|
+
values.push(staticEval(value, context));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return values;
|
|
32
|
+
},
|
|
33
|
+
[SyntaxKind.ObjectLiteralExpression]: (node: ObjectLiteralExpression, context) => {
|
|
34
|
+
const result: Dictionary<unknown> = {};
|
|
35
|
+
for (const property of node.getProperties()) {
|
|
36
|
+
Object.assign(result, staticEval(property, context));
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
},
|
|
40
|
+
[SyntaxKind.StringLiteral]: (node) => node.getLiteralValue(),
|
|
41
|
+
[SyntaxKind.PropertyAssignment]: (node, context) => ({
|
|
42
|
+
[node.getName()]: staticEval(node.getInitializer(), context),
|
|
43
|
+
}),
|
|
44
|
+
[SyntaxKind.ShorthandPropertyAssignment]: (node: ShorthandPropertyAssignment, context) => ({
|
|
45
|
+
[node.getName()]: staticEval(node.getNameNode(), context),
|
|
46
|
+
}),
|
|
47
|
+
[SyntaxKind.SpreadElement]: (node, context) => staticEval(node.getExpression(), context),
|
|
48
|
+
[SyntaxKind.SpreadAssignment]: (node, context) => staticEval(node.getExpression(), context),
|
|
49
|
+
[SyntaxKind.Identifier]: (node: Identifier, context) => {
|
|
50
|
+
switch (node.getText()) {
|
|
51
|
+
case 'undefined':
|
|
52
|
+
return undefined;
|
|
53
|
+
case 'process':
|
|
54
|
+
return process;
|
|
55
|
+
}
|
|
56
|
+
const definitionNodes = node.getDefinitionNodes();
|
|
57
|
+
if (!definitionNodes.length) {
|
|
58
|
+
throw new Error(`No definition node found for identifier ${node.getText()}.`);
|
|
59
|
+
}
|
|
60
|
+
return staticEval(definitionNodes[0], context);
|
|
61
|
+
},
|
|
62
|
+
[SyntaxKind.ParenthesizedExpression]: (node, context) => staticEval(node.getExpression(), context),
|
|
63
|
+
[SyntaxKind.AsExpression]: (node, context) => staticEval(node.getExpression(), context),
|
|
64
|
+
[SyntaxKind.ConditionalExpression]: (node, context) =>
|
|
65
|
+
staticEval(node.getCondition(), context)
|
|
66
|
+
? staticEval(node.getWhenTrue(), context)
|
|
67
|
+
: staticEval(node.getWhenFalse(), context),
|
|
68
|
+
[SyntaxKind.TrueKeyword]: () => true,
|
|
69
|
+
[SyntaxKind.FalseKeyword]: () => false,
|
|
70
|
+
[SyntaxKind.NumericLiteral]: (node) => node.getLiteralValue(),
|
|
71
|
+
[SyntaxKind.CallExpression]: (node, context) => {
|
|
72
|
+
const method = staticEval(node.getExpression(), context) as (...args: unknown[]) => unknown;
|
|
73
|
+
const args = node.getArguments().map((arg) => staticEval(arg, context));
|
|
74
|
+
return method(...args);
|
|
75
|
+
},
|
|
76
|
+
[SyntaxKind.PropertyAccessExpression]: (node, context) => {
|
|
77
|
+
const target = staticEval(node.getExpression(), context);
|
|
78
|
+
const property = target[node.getName()];
|
|
79
|
+
if (typeof property === 'function') {
|
|
80
|
+
if (Array.isArray(target)) {
|
|
81
|
+
switch (node.getName()) {
|
|
82
|
+
case 'map':
|
|
83
|
+
case 'flatMap':
|
|
84
|
+
case 'includes':
|
|
85
|
+
case 'some':
|
|
86
|
+
case 'find':
|
|
87
|
+
case 'filter':
|
|
88
|
+
return target[node.getName()].bind(target);
|
|
89
|
+
}
|
|
90
|
+
} else if (typeof target === 'string') {
|
|
91
|
+
const name = node.getName() as keyof string;
|
|
92
|
+
switch (name) {
|
|
93
|
+
case 'slice':
|
|
94
|
+
case 'toUpperCase':
|
|
95
|
+
case 'toLowerCase':
|
|
96
|
+
return target[name].bind(target);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Cannot handle method ${node.getName()} on type ${typeof target}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return property;
|
|
103
|
+
},
|
|
104
|
+
[SyntaxKind.ArrowFunction]: (node, context) => {
|
|
105
|
+
return (...args: unknown[]) => {
|
|
106
|
+
const parameters: Dictionary<unknown> = {};
|
|
107
|
+
let i = 0;
|
|
108
|
+
for (const parameter of node.getParameters()) {
|
|
109
|
+
parameters[parameter.getName()] = args[i];
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
return staticEval(node.getBody(), { ...context, ...parameters });
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
[SyntaxKind.Block]: (node, context) => {
|
|
116
|
+
for (const statement of node.getStatements()) {
|
|
117
|
+
return staticEval(statement, context);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[SyntaxKind.CaseClause]: (node, context) => {
|
|
121
|
+
const statements = node.getStatements();
|
|
122
|
+
if (statements.length !== 1) {
|
|
123
|
+
console.error(node.getText());
|
|
124
|
+
throw new Error(`Can only handle code blocks with 1 statement.`);
|
|
125
|
+
}
|
|
126
|
+
return staticEval(statements[0], context);
|
|
127
|
+
},
|
|
128
|
+
[SyntaxKind.DefaultClause]: (node, context) => {
|
|
129
|
+
const statements = node.getStatements();
|
|
130
|
+
if (statements.length !== 1) {
|
|
131
|
+
console.error(node.getText());
|
|
132
|
+
throw new Error(`Can only handle code blocks with exactly 1 statement.`);
|
|
133
|
+
}
|
|
134
|
+
return staticEval(statements[0], context);
|
|
135
|
+
},
|
|
136
|
+
[SyntaxKind.ReturnStatement]: (node, context) => {
|
|
137
|
+
return staticEval(node.getExpression(), context);
|
|
138
|
+
},
|
|
139
|
+
[SyntaxKind.SwitchStatement]: (node, context) => {
|
|
140
|
+
const value = staticEval(node.getExpression(), context);
|
|
141
|
+
let active = false;
|
|
142
|
+
for (const clause of node.getCaseBlock().getClauses()) {
|
|
143
|
+
switch (clause.getKind()) {
|
|
144
|
+
case SyntaxKind.DefaultClause:
|
|
145
|
+
return staticEval(clause, context);
|
|
146
|
+
case SyntaxKind.CaseClause: {
|
|
147
|
+
const caseClause: CaseClause = clause.asKindOrThrow(SyntaxKind.CaseClause);
|
|
148
|
+
if (caseClause.getStatements().length && active) {
|
|
149
|
+
return staticEval(clause, context);
|
|
150
|
+
}
|
|
151
|
+
const caseValue = staticEval(caseClause.getExpression(), context);
|
|
152
|
+
if (value === caseValue) {
|
|
153
|
+
active = true;
|
|
154
|
+
if (caseClause.getStatements().length) {
|
|
155
|
+
return staticEval(clause, context);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
[SyntaxKind.Parameter]: (node, context) => context[node.getName()],
|
|
163
|
+
[SyntaxKind.BinaryExpression]: (node, context) => {
|
|
164
|
+
switch (node.getOperatorToken().getKind()) {
|
|
165
|
+
case SyntaxKind.EqualsEqualsEqualsToken:
|
|
166
|
+
return staticEval(node.getLeft(), context) === staticEval(node.getRight(), context);
|
|
167
|
+
case SyntaxKind.BarBarToken:
|
|
168
|
+
return staticEval(node.getLeft(), context) || staticEval(node.getRight(), context);
|
|
169
|
+
default:
|
|
170
|
+
throw new Error(`Cannot handle operator of kind ${node.getOperatorToken().getKindName()}`);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
[SyntaxKind.SatisfiesExpression]: (node, context) => staticEval(node.getExpression(), context),
|
|
174
|
+
[SyntaxKind.TemplateExpression]: (node: TemplateExpression, context) =>
|
|
175
|
+
node.getHead().getLiteralText() +
|
|
176
|
+
node
|
|
177
|
+
.getTemplateSpans()
|
|
178
|
+
.map((span) => (staticEval(span.getExpression(), context) as string) + staticEval(span.getLiteral(), context))
|
|
179
|
+
.join(''),
|
|
180
|
+
[SyntaxKind.TemplateTail]: (node: TemplateTail) => node.getLiteralText(),
|
|
181
|
+
[SyntaxKind.TemplateMiddle]: (node) => node.getLiteralText(),
|
|
182
|
+
[SyntaxKind.PrefixUnaryExpression]: (node: PrefixUnaryExpression, context) => {
|
|
183
|
+
switch (node.getOperatorToken()) {
|
|
184
|
+
case SyntaxKind.PlusToken:
|
|
185
|
+
return +staticEval(node.getOperand(), context);
|
|
186
|
+
case SyntaxKind.MinusToken:
|
|
187
|
+
return -staticEval(node.getOperand(), context);
|
|
188
|
+
case SyntaxKind.TildeToken:
|
|
189
|
+
return ~staticEval(node.getOperand(), context);
|
|
190
|
+
case SyntaxKind.ExclamationToken:
|
|
191
|
+
return !staticEval(node.getOperand(), context);
|
|
192
|
+
case SyntaxKind.PlusPlusToken:
|
|
193
|
+
case SyntaxKind.MinusMinusToken:
|
|
194
|
+
throw new Error(`Cannot handle assignments.`);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[SyntaxKind.ElementAccessExpression]: (node: ElementAccessExpression, context) => {
|
|
198
|
+
const target = staticEval(node.getExpression(), context);
|
|
199
|
+
const argument = staticEval(node.getArgumentExpression(), context) as string;
|
|
200
|
+
return target[argument];
|
|
201
|
+
},
|
|
202
|
+
[SyntaxKind.NoSubstitutionTemplateLiteral]: (node) => node.getLiteralValue(),
|
|
203
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const EMPTY_MODELS = `
|
|
2
|
+
import { RawModels, getModels } from '@smartive/graphql-magic';
|
|
3
|
+
|
|
4
|
+
export const rawModels: RawModels = [
|
|
5
|
+
{
|
|
6
|
+
kind: 'entity',
|
|
7
|
+
name: 'User',
|
|
8
|
+
fields: []
|
|
9
|
+
},
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export const models = getModels(rawModels);
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const GRAPHQL_CODEGEN = (path: string) => `
|
|
16
|
+
overwrite: true
|
|
17
|
+
schema: '${path}/schema.graphql'
|
|
18
|
+
documents: null
|
|
19
|
+
generates:
|
|
20
|
+
${path}/api/index.ts:
|
|
21
|
+
plugins:
|
|
22
|
+
- 'typescript'
|
|
23
|
+
- 'typescript-resolvers'
|
|
24
|
+
- add:
|
|
25
|
+
content: "import { DateTime } from 'luxon'"
|
|
26
|
+
config:
|
|
27
|
+
scalars:
|
|
28
|
+
DateTime: DateTime
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const CLIENT_CODEGEN = (path: string) => `
|
|
32
|
+
schema: ${path}/schema.graphql
|
|
33
|
+
documents: [ './src/**/*.ts', './src/**/*.tsx' ]
|
|
34
|
+
generates:
|
|
35
|
+
${path}/client/index.ts:
|
|
36
|
+
plugins:
|
|
37
|
+
- typescript
|
|
38
|
+
- typescript-operations
|
|
39
|
+
- typescript-compatibility
|
|
40
|
+
|
|
41
|
+
config:
|
|
42
|
+
preResolveTypes: true # Simplifies the generated types
|
|
43
|
+
namingConvention: keep # Keeps naming as-is
|
|
44
|
+
nonOptionalTypename: true # Forces \`__typename\` on all selection sets
|
|
45
|
+
skipTypeNameForRoot: true # Don't generate __typename for root types
|
|
46
|
+
avoidOptionals: # Avoids optionals on the level of the field
|
|
47
|
+
field: true
|
|
48
|
+
scalars:
|
|
49
|
+
DateTime: string
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
export const KNEXFILE = `
|
|
53
|
+
const config = {
|
|
54
|
+
client: 'postgresql',
|
|
55
|
+
connection: {
|
|
56
|
+
host: process.env.DATABASE_HOST,
|
|
57
|
+
database: process.env.DATABASE_NAME,
|
|
58
|
+
user: process.env.DATABASE_USER,
|
|
59
|
+
password: process.env.DATABASE_PASSWORD,
|
|
60
|
+
},
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
export default config;
|
|
64
|
+
`;
|
package/src/gqm/utils.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SourceFile, SyntaxKind, SyntaxList } from 'ts-morph';
|
|
2
|
+
|
|
3
|
+
export const findDeclarationInFile = (sourceFile: SourceFile, name: string) => {
|
|
4
|
+
const syntaxList = sourceFile.getChildrenOfKind(SyntaxKind.SyntaxList)[0];
|
|
5
|
+
if (!syntaxList) {
|
|
6
|
+
throw new Error('No SyntaxList');
|
|
7
|
+
}
|
|
8
|
+
const declaration = findDeclaration(syntaxList, name);
|
|
9
|
+
if (!declaration) {
|
|
10
|
+
throw new Error(`No ${name} declaration`);
|
|
11
|
+
}
|
|
12
|
+
return declaration;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const findDeclaration = (syntaxList: SyntaxList, name: string) => {
|
|
16
|
+
for (const variableStatement of syntaxList.getChildrenOfKind(SyntaxKind.VariableStatement)) {
|
|
17
|
+
for (const declaration of variableStatement.getDeclarationList().getDeclarations()) {
|
|
18
|
+
if (declaration.getName() === name) {
|
|
19
|
+
return declaration;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { KindToNodeMappings, Node, SyntaxKind } from 'ts-morph';
|
|
2
|
+
import { get } from '..';
|
|
3
|
+
|
|
4
|
+
export type Visitor<T, C> = {
|
|
5
|
+
undefined?: () => T;
|
|
6
|
+
unknown?: (node: Node) => T;
|
|
7
|
+
} & {
|
|
8
|
+
[kind in keyof KindToNodeMappings]?: (node: KindToNodeMappings[kind], context: C) => T;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const visit = <T, C>(node: Node | undefined, context: C, visitor: Visitor<T, C>) => {
|
|
12
|
+
const kind: undefined | keyof KindToNodeMappings = node?.getKind();
|
|
13
|
+
if (kind in visitor) {
|
|
14
|
+
return visitor[kind](node.asKindOrThrow(kind), context);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if ('unknown' in visitor) {
|
|
18
|
+
return visitor.unknown(node);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.error(node.getText());
|
|
22
|
+
console.error(node.getParent().getText());
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Cannot handle kind ${get(
|
|
25
|
+
Object.entries(SyntaxKind).find(([, val]) => val === kind),
|
|
26
|
+
0
|
|
27
|
+
)}`
|
|
28
|
+
);
|
|
29
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -131,7 +131,7 @@ export class MigrationGenerator {
|
|
|
131
131
|
model,
|
|
132
132
|
model.fields.filter(
|
|
133
133
|
({ name, ...field }) =>
|
|
134
|
-
field.kind !== '
|
|
134
|
+
field.kind !== 'custom' &&
|
|
135
135
|
!this.columns[model.name].some(
|
|
136
136
|
(col) => col.name === (field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
|
|
137
137
|
)
|
|
@@ -198,7 +198,7 @@ export class MigrationGenerator {
|
|
|
198
198
|
const revisionTable = `${model.name}Revision`;
|
|
199
199
|
const missingRevisionFields = model.fields.filter(
|
|
200
200
|
({ name, updatable, ...field }) =>
|
|
201
|
-
field.kind !== '
|
|
201
|
+
field.kind !== 'custom' &&
|
|
202
202
|
updatable &&
|
|
203
203
|
!this.columns[revisionTable].some(
|
|
204
204
|
(col) => col.name === (field.kind === 'relation' ? field.foreignKey || `${name}Id` : name)
|
|
@@ -210,7 +210,7 @@ export class MigrationGenerator {
|
|
|
210
210
|
const revisionFieldsToRemove = model.fields.filter(
|
|
211
211
|
({ name, updatable, generated, ...field }) =>
|
|
212
212
|
!generated &&
|
|
213
|
-
field.kind !== '
|
|
213
|
+
field.kind !== 'custom' &&
|
|
214
214
|
!updatable &&
|
|
215
215
|
!(field.kind === 'relation' && field.foreignKey === 'id') &&
|
|
216
216
|
this.columns[revisionTable].some(
|
|
@@ -580,6 +580,7 @@ export class MigrationGenerator {
|
|
|
580
580
|
};
|
|
581
581
|
const kind = field.kind;
|
|
582
582
|
switch (kind) {
|
|
583
|
+
case undefined:
|
|
583
584
|
case 'primitive':
|
|
584
585
|
switch (field.type) {
|
|
585
586
|
case 'Boolean':
|
|
@@ -636,8 +637,8 @@ export class MigrationGenerator {
|
|
|
636
637
|
case 'json':
|
|
637
638
|
this.writer.write(`table.json('${typeToField(field.type)}')`);
|
|
638
639
|
break;
|
|
639
|
-
case '
|
|
640
|
-
throw new Error("
|
|
640
|
+
case 'custom':
|
|
641
|
+
throw new Error("Custom fields aren't stored in the database");
|
|
641
642
|
default: {
|
|
642
643
|
const exhaustiveCheck: never = kind;
|
|
643
644
|
throw new Error(exhaustiveCheck);
|
|
@@ -645,3 +646,15 @@ export class MigrationGenerator {
|
|
|
645
646
|
}
|
|
646
647
|
}
|
|
647
648
|
}
|
|
649
|
+
|
|
650
|
+
export const getMigrationDate = () => {
|
|
651
|
+
const date = new Date();
|
|
652
|
+
const year = date.getFullYear();
|
|
653
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
654
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
655
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
656
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
657
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
658
|
+
|
|
659
|
+
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
660
|
+
};
|
package/src/models/models.ts
CHANGED
|
@@ -14,11 +14,15 @@ export type RawModel = {
|
|
|
14
14
|
| { kind: 'raw-enum'; values: string[] }
|
|
15
15
|
| { kind: 'interface'; fields: ModelField[] }
|
|
16
16
|
| {
|
|
17
|
-
kind: '
|
|
18
|
-
fields:
|
|
17
|
+
kind: 'input';
|
|
18
|
+
fields: ObjectField[];
|
|
19
19
|
}
|
|
20
20
|
| {
|
|
21
21
|
kind: 'object';
|
|
22
|
+
fields: ObjectField[];
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
kind: 'entity';
|
|
22
26
|
interfaces?: string[];
|
|
23
27
|
queriable?: boolean;
|
|
24
28
|
listQueriable?: boolean;
|
|
@@ -41,8 +45,9 @@ export type ScalarModel = Extract<RawModel, { kind: 'scalar' }>;
|
|
|
41
45
|
export type EnumModel = Extract<RawModel, { kind: 'enum' }>;
|
|
42
46
|
export type RawEnumModel = Extract<RawModel, { kind: 'raw-enum' }>;
|
|
43
47
|
export type InterfaceModel = Extract<RawModel, { kind: 'interface' }>;
|
|
44
|
-
export type RawObjectModel = Extract<RawModel, { kind: 'raw' }>;
|
|
45
48
|
export type ObjectModel = Extract<RawModel, { kind: 'object' }>;
|
|
49
|
+
export type InputModel = Extract<RawModel, { kind: 'input' }>;
|
|
50
|
+
export type EntityModel = Extract<RawModel, { kind: 'entity' }>;
|
|
46
51
|
|
|
47
52
|
type BaseNumberType = {
|
|
48
53
|
unit?: 'million';
|
|
@@ -81,9 +86,9 @@ type FieldBase2 =
|
|
|
81
86
|
| { type: 'Upload' }
|
|
82
87
|
))
|
|
83
88
|
| { kind: 'enum'; type: string; possibleValues?: Value[] }
|
|
84
|
-
| { kind: '
|
|
89
|
+
| { kind: 'custom'; type: string };
|
|
85
90
|
|
|
86
|
-
export type
|
|
91
|
+
export type ObjectField = FieldBase & FieldBase2;
|
|
87
92
|
|
|
88
93
|
export type ModelField = FieldBase &
|
|
89
94
|
(
|
|
@@ -149,12 +154,12 @@ export type FloatField = Extract<PrimitiveField, { type: 'Float' }>;
|
|
|
149
154
|
export type UploadField = Extract<PrimitiveField, { type: 'Upload' }>;
|
|
150
155
|
export type JsonField = Extract<ModelField, { kind: 'json' }>;
|
|
151
156
|
export type EnumField = Extract<ModelField, { kind: 'enum' }>;
|
|
152
|
-
export type
|
|
157
|
+
export type CustomField = Extract<ModelField, { kind: 'custom' }>;
|
|
153
158
|
export type RelationField = Extract<ModelField, { kind: 'relation' }>;
|
|
154
159
|
|
|
155
160
|
export type Models = Model[];
|
|
156
161
|
|
|
157
|
-
export type Model =
|
|
162
|
+
export type Model = EntityModel & {
|
|
158
163
|
fieldsByName: Record<string, ModelField>;
|
|
159
164
|
relations: Relation[];
|
|
160
165
|
relationsByName: Record<string, Relation>;
|