@aexol/axolotl-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +5 -0
- package/gen.test.ts +54 -0
- package/gen.ts +103 -0
- package/index.ts +92 -0
- package/jest.config.js +10 -0
- package/package.json +16 -0
- package/tsconfig.json +44 -0
package/.eslintrc.json
ADDED
package/gen.test.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import { resolveFieldType } from './gen.js';
|
|
3
|
+
import { Options } from 'graphql-js-tree';
|
|
4
|
+
import * as assert from 'node:assert';
|
|
5
|
+
|
|
6
|
+
test('resolveFieldType', (t, done) => {
|
|
7
|
+
const possibleVariants = {
|
|
8
|
+
['Person | undefined']: resolveFieldType('Person', {
|
|
9
|
+
type: Options.name,
|
|
10
|
+
name: 'Person',
|
|
11
|
+
}),
|
|
12
|
+
['Person']: resolveFieldType('Person', {
|
|
13
|
+
type: Options.required,
|
|
14
|
+
nest: {
|
|
15
|
+
type: Options.name,
|
|
16
|
+
name: 'Person',
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
['Array<Person | undefined> | undefined']: resolveFieldType('Person', {
|
|
20
|
+
type: Options.array,
|
|
21
|
+
nest: {
|
|
22
|
+
type: Options.name,
|
|
23
|
+
name: 'Person',
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
['Array<Person | undefined>']: resolveFieldType('Person', {
|
|
27
|
+
type: Options.required,
|
|
28
|
+
nest: {
|
|
29
|
+
type: Options.array,
|
|
30
|
+
nest: {
|
|
31
|
+
type: Options.name,
|
|
32
|
+
name: 'Person',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
['Array<Person>']: resolveFieldType('Person', {
|
|
37
|
+
type: Options.required,
|
|
38
|
+
nest: {
|
|
39
|
+
type: Options.array,
|
|
40
|
+
nest: {
|
|
41
|
+
type: Options.required,
|
|
42
|
+
nest: {
|
|
43
|
+
type: Options.name,
|
|
44
|
+
name: 'Person',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
Object.entries(possibleVariants).forEach(([k, v]) => {
|
|
51
|
+
assert.equal(k, v);
|
|
52
|
+
});
|
|
53
|
+
done();
|
|
54
|
+
});
|
package/gen.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { FieldType, Options, Parser, ParserField, TypeDefinition, getTypeName } from 'graphql-js-tree';
|
|
3
|
+
|
|
4
|
+
const TAB = (n: number) =>
|
|
5
|
+
new Array(n)
|
|
6
|
+
.fill(1)
|
|
7
|
+
.map(() => ' ')
|
|
8
|
+
.join('');
|
|
9
|
+
|
|
10
|
+
const toTsType = (t: string) => {
|
|
11
|
+
if (t === 'String') return 'string';
|
|
12
|
+
if (t === 'Int') return 'number';
|
|
13
|
+
if (t === 'Float') return 'number';
|
|
14
|
+
if (t === 'ID') return 'unknown';
|
|
15
|
+
if (t === 'Boolean') return 'boolean';
|
|
16
|
+
return t;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const resolveFieldType = (
|
|
20
|
+
name: string,
|
|
21
|
+
fType: FieldType,
|
|
22
|
+
fn: (str: string) => string = (x) => x,
|
|
23
|
+
isRequired = false,
|
|
24
|
+
): string => {
|
|
25
|
+
if (fType.type === Options.name) {
|
|
26
|
+
return fn(isRequired ? name : `${name} | undefined`);
|
|
27
|
+
}
|
|
28
|
+
if (fType.type === Options.array) {
|
|
29
|
+
return resolveFieldType(
|
|
30
|
+
name,
|
|
31
|
+
fType.nest,
|
|
32
|
+
isRequired ? (x) => `Array<${fn(x)}>` : (x) => `Array<${fn(x)}> | undefined`,
|
|
33
|
+
false,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (fType.type === Options.required) {
|
|
37
|
+
return resolveFieldType(name, fType.nest, fn, true);
|
|
38
|
+
}
|
|
39
|
+
throw new Error('Invalid field type');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const resolveField = (f: ParserField): string => {
|
|
43
|
+
const isNullType = (type: string): string => {
|
|
44
|
+
return f.type.fieldType.type === Options.required ? `: ${type}` : `?: ${type}`;
|
|
45
|
+
};
|
|
46
|
+
return `${TAB(1)}${f.name}${isNullType(resolveFieldType(toTsType(getTypeName(f.type.fieldType)), f.type.fieldType))}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const buildArgs = (args: ParserField[]) => {
|
|
50
|
+
if (args.length === 0) return 'never;';
|
|
51
|
+
const inputFields = args.map((a) => `${TAB(3)}${resolveField(a)};`);
|
|
52
|
+
return `{\n${inputFields.join('\n')}\n${TAB(3)}};`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const buildEnumArgs = (args: ParserField[]) => {
|
|
56
|
+
if (args.length === 0) return 'never';
|
|
57
|
+
const inputFields = args.map((a) => `${TAB(1)}${a.name} = "${a.name}"`);
|
|
58
|
+
return `{\n${inputFields.join(',\n')}\n}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const generateModelsString = (fileContent: string) => {
|
|
62
|
+
const { nodes } = Parser.parse(fileContent);
|
|
63
|
+
|
|
64
|
+
const scalars = nodes.filter((n) => n.data.type === TypeDefinition.ScalarTypeDefinition);
|
|
65
|
+
const scalarsString = scalars.map((s) => `export type ${s.name} = unknown`).join('\n');
|
|
66
|
+
|
|
67
|
+
const enums = nodes.filter((n) => n.data.type === TypeDefinition.EnumTypeDefinition);
|
|
68
|
+
const enumsString = enums.map((s) => `export enum ${s.name} ${buildEnumArgs(s.args)}`).join('\n');
|
|
69
|
+
|
|
70
|
+
const inputs = nodes.filter((n) => n.data.type === TypeDefinition.InputObjectTypeDefinition);
|
|
71
|
+
const inputsString = inputs
|
|
72
|
+
.map((i) => {
|
|
73
|
+
const inputFields = i.args.map((a) => `${resolveField(a)};`);
|
|
74
|
+
return `export interface ${i.name} {\n${inputFields.join('\n')}\n}`;
|
|
75
|
+
})
|
|
76
|
+
.join('\n');
|
|
77
|
+
|
|
78
|
+
const types = nodes.filter((n) => n.data.type === TypeDefinition.ObjectTypeDefinition);
|
|
79
|
+
const typesString = types
|
|
80
|
+
.map((t) => {
|
|
81
|
+
const typeFields = t.args.map((a) => {
|
|
82
|
+
return `${TAB(2)}${a.name}: {\n${TAB(3)}args: ${buildArgs(a.args)}\n${TAB(2)}};`;
|
|
83
|
+
});
|
|
84
|
+
return `${TAB(1)}['${t.name}']: {\n${typeFields.join('\n')}\n${TAB(1)}};`;
|
|
85
|
+
})
|
|
86
|
+
.join('\n');
|
|
87
|
+
|
|
88
|
+
const typesFullString = `export type Models = {\n${typesString}\n};\n`;
|
|
89
|
+
|
|
90
|
+
return [scalarsString, enumsString, inputsString, typesFullString].filter(Boolean).join('\n\n');
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const generateModels = ({
|
|
94
|
+
schemaPath = './schema.graphql',
|
|
95
|
+
modelsPath = './models.ts',
|
|
96
|
+
}: {
|
|
97
|
+
schemaPath: string;
|
|
98
|
+
modelsPath: string;
|
|
99
|
+
}) => {
|
|
100
|
+
const fileContent = readFileSync(schemaPath, 'utf8');
|
|
101
|
+
const modelsString = generateModelsString(fileContent);
|
|
102
|
+
writeFileSync(modelsPath, modelsString);
|
|
103
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
|
|
4
|
+
import { generateModels } from '@/gen.js';
|
|
5
|
+
|
|
6
|
+
interface CustomHandler<InputType, ArgumentsType = unknown> {
|
|
7
|
+
(input: InputType, args: ArgumentsType extends { args: infer R } ? R : never): any;
|
|
8
|
+
}
|
|
9
|
+
interface CustomMiddlewareHandler<InputType> {
|
|
10
|
+
(input: InputType): InputType;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ResolversUnknown<InputType> = {
|
|
14
|
+
[x: string]: {
|
|
15
|
+
[x: string]: (input: InputType, args?: any) => any | undefined | Promise<any | undefined>;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type InferAdapterType<ADAPTER extends (passedResolvers: ResolversUnknown<any>, production?: boolean) => any> =
|
|
20
|
+
Parameters<ADAPTER>[0] extends {
|
|
21
|
+
[x: string]: {
|
|
22
|
+
[y: string]: infer R;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
? R extends (...args: any[]) => any
|
|
26
|
+
? Parameters<R>[0]
|
|
27
|
+
: never
|
|
28
|
+
: never;
|
|
29
|
+
|
|
30
|
+
export const AxolotlAdapter =
|
|
31
|
+
<Inp>() =>
|
|
32
|
+
<T>(fn: (passedResolvers: ResolversUnknown<Inp>, production?: boolean) => T) =>
|
|
33
|
+
fn;
|
|
34
|
+
|
|
35
|
+
export { generateModels };
|
|
36
|
+
|
|
37
|
+
export const Axolotl =
|
|
38
|
+
<ADAPTER extends (passedResolvers: ResolversUnknown<any>, production?: boolean) => any>(adapter: ADAPTER) =>
|
|
39
|
+
<Models>({
|
|
40
|
+
production,
|
|
41
|
+
schemaPath,
|
|
42
|
+
modelsPath,
|
|
43
|
+
}: {
|
|
44
|
+
// input is only required for frameworks with external routing
|
|
45
|
+
schemaPath: string;
|
|
46
|
+
modelsPath: string;
|
|
47
|
+
// Instead of controlling developer and production mode by some force envs we allow to control it however you want. Generators don't run on production
|
|
48
|
+
production?: boolean;
|
|
49
|
+
}) => {
|
|
50
|
+
type Inp = InferAdapterType<ADAPTER>;
|
|
51
|
+
type Resolvers = {
|
|
52
|
+
[P in keyof Models]?: {
|
|
53
|
+
[T in keyof Models[P]]?: CustomHandler<Inp, Models[P][T]>;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
type Handler = CustomHandler<Inp>;
|
|
57
|
+
type MiddlewareHandler = CustomMiddlewareHandler<Inp>;
|
|
58
|
+
|
|
59
|
+
const createResolvers = <Z>(k: Z | Resolvers) => k as Z;
|
|
60
|
+
|
|
61
|
+
if (!production) {
|
|
62
|
+
// We need to generate models for Axolotl to work this is called with CLI but also on every dev run to keep things in sync
|
|
63
|
+
generateModels({ schemaPath, modelsPath });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const applyMiddleware = <Z>(
|
|
67
|
+
r: Z | Resolvers,
|
|
68
|
+
middlewares: MiddlewareHandler[],
|
|
69
|
+
k: {
|
|
70
|
+
[P in keyof Z]?: {
|
|
71
|
+
[Y in keyof Z[P]]?: true;
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
) => {
|
|
75
|
+
Object.entries(k).forEach(([typeName, fields]) => {
|
|
76
|
+
Object.keys(fields as Record<string, true>).forEach((fieldName) => {
|
|
77
|
+
const oldResolver = (r as Record<string, Record<string, Handler>>)[typeName][fieldName];
|
|
78
|
+
(r as Record<string, Record<string, Handler>>)[typeName][fieldName] = middlewares.reduce((a, b) => {
|
|
79
|
+
return (input, args) => {
|
|
80
|
+
const middlewaredInput = b(input);
|
|
81
|
+
return a(middlewaredInput, args);
|
|
82
|
+
};
|
|
83
|
+
}, oldResolver);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
createResolvers,
|
|
90
|
+
applyMiddleware,
|
|
91
|
+
};
|
|
92
|
+
};
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aexol/axolotl-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "./lib/index.js",
|
|
6
|
+
"author": "Aexol, Artur Czemiel",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tspc",
|
|
10
|
+
"start": "tspc --watch",
|
|
11
|
+
"lint": "tspc && eslint \"./src/**/*.{ts,js}\" --quiet --fix"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"graphql-js-tree": "^1.0.6"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"sourceMap": true,
|
|
4
|
+
"target": "es2022",
|
|
5
|
+
"module": "es2022",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"incremental": true,
|
|
10
|
+
"removeComments": true,
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"strictNullChecks": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"outDir": "./lib",
|
|
16
|
+
"lib": [
|
|
17
|
+
"ESNext",
|
|
18
|
+
"DOM",
|
|
19
|
+
"DOM.Iterable"
|
|
20
|
+
],
|
|
21
|
+
"rootDir": "./",
|
|
22
|
+
"baseUrl": "./",
|
|
23
|
+
"composite": true,
|
|
24
|
+
"paths": {
|
|
25
|
+
"@/*": [
|
|
26
|
+
"./*"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"plugins": [
|
|
30
|
+
{
|
|
31
|
+
"transform": "typescript-transform-paths"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"transform": "typescript-transform-paths",
|
|
35
|
+
"afterDeclarations": true
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"exclude": [
|
|
40
|
+
"lib",
|
|
41
|
+
"node_modules",
|
|
42
|
+
"jest.config.js",
|
|
43
|
+
]
|
|
44
|
+
}
|