voltjs-framework 1.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/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
package/bin/volt.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VoltJS CLI - The command center for VoltJS framework
|
|
5
|
+
* Usage:
|
|
6
|
+
* volt create <project-name> Create a new VoltJS project
|
|
7
|
+
* volt dev Start development server with hot reload
|
|
8
|
+
* volt build Build for production
|
|
9
|
+
* volt generate <type> <name> Generate components, pages, APIs
|
|
10
|
+
* volt db:migrate Run database migrations
|
|
11
|
+
* volt db:seed Seed the database
|
|
12
|
+
* volt lint Lint your project
|
|
13
|
+
* volt test Run tests
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
|
|
21
|
+
// CLI colors without dependencies
|
|
22
|
+
const c = {
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
bold: '\x1b[1m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
blue: '\x1b[34m',
|
|
30
|
+
magenta: '\x1b[35m',
|
|
31
|
+
cyan: '\x1b[36m',
|
|
32
|
+
white: '\x1b[37m',
|
|
33
|
+
bgBlue: '\x1b[44m',
|
|
34
|
+
bgMagenta: '\x1b[45m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function logo() {
|
|
38
|
+
console.log(`
|
|
39
|
+
${c.cyan}${c.bold}
|
|
40
|
+
██╗ ██╗ ██████╗ ██╗ ████████╗ ██╗███████╗
|
|
41
|
+
██║ ██║██╔═══██╗██║ ╚══██╔══╝ ██║██╔════╝
|
|
42
|
+
██║ ██║██║ ██║██║ ██║ ██║███████╗
|
|
43
|
+
╚██╗ ██╔╝██║ ██║██║ ██║ ██ ██║╚════██║
|
|
44
|
+
╚████╔╝ ╚██████╔╝███████╗██║ ╚█████╔╝███████║
|
|
45
|
+
╚═══╝ ╚═════╝ ╚══════╝╚═╝ ╚════╝ ╚══════╝
|
|
46
|
+
${c.reset}
|
|
47
|
+
${c.dim}Lightning-fast, batteries-included framework${c.reset}
|
|
48
|
+
${c.dim}Version 1.0.0${c.reset}
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const command = args[0];
|
|
54
|
+
|
|
55
|
+
if (!command || command === '--help' || command === '-h') {
|
|
56
|
+
logo();
|
|
57
|
+
console.log(`${c.bold}Usage:${c.reset}
|
|
58
|
+
${c.cyan}volt create${c.reset} <project-name> Create a new VoltJS project
|
|
59
|
+
${c.cyan}volt dev${c.reset} Start dev server (port 3000)
|
|
60
|
+
${c.cyan}volt build${c.reset} Build for production
|
|
61
|
+
${c.cyan}volt start${c.reset} Start production server
|
|
62
|
+
${c.cyan}volt generate${c.reset} <type> <name> Generate code (page, api, component, model)
|
|
63
|
+
${c.cyan}volt db:migrate${c.reset} Run database migrations
|
|
64
|
+
${c.cyan}volt db:seed${c.reset} Run database seeders
|
|
65
|
+
${c.cyan}volt db:rollback${c.reset} Rollback last migration
|
|
66
|
+
${c.cyan}volt lint${c.reset} Lint project files
|
|
67
|
+
${c.cyan}volt test${c.reset} Run tests
|
|
68
|
+
${c.cyan}volt routes${c.reset} List all registered routes
|
|
69
|
+
${c.cyan}volt --version${c.reset} Show version
|
|
70
|
+
|
|
71
|
+
${c.bold}Examples:${c.reset}
|
|
72
|
+
${c.dim}$ volt create my-app${c.reset}
|
|
73
|
+
${c.dim}$ volt generate page dashboard${c.reset}
|
|
74
|
+
${c.dim}$ volt generate api users${c.reset}
|
|
75
|
+
${c.dim}$ volt generate component Header${c.reset}
|
|
76
|
+
`);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (command === '--version' || command === '-v') {
|
|
81
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
82
|
+
console.log(`VoltJS v${pkg.version}`);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Route to appropriate CLI handler
|
|
87
|
+
try {
|
|
88
|
+
switch (command) {
|
|
89
|
+
case 'create':
|
|
90
|
+
require('../src/cli/create')(args.slice(1));
|
|
91
|
+
break;
|
|
92
|
+
case 'dev':
|
|
93
|
+
require('../src/cli/dev')(args.slice(1));
|
|
94
|
+
break;
|
|
95
|
+
case 'build':
|
|
96
|
+
require('../src/cli/build')(args.slice(1));
|
|
97
|
+
break;
|
|
98
|
+
case 'start':
|
|
99
|
+
require('../src/cli/start')(args.slice(1));
|
|
100
|
+
break;
|
|
101
|
+
case 'generate':
|
|
102
|
+
case 'g':
|
|
103
|
+
require('../src/cli/generate')(args.slice(1));
|
|
104
|
+
break;
|
|
105
|
+
case 'db:migrate':
|
|
106
|
+
require('../src/cli/db')([ 'migrate', ...args.slice(1) ]);
|
|
107
|
+
break;
|
|
108
|
+
case 'db:seed':
|
|
109
|
+
require('../src/cli/db')([ 'seed', ...args.slice(1) ]);
|
|
110
|
+
break;
|
|
111
|
+
case 'db:rollback':
|
|
112
|
+
require('../src/cli/db')([ 'rollback', ...args.slice(1) ]);
|
|
113
|
+
break;
|
|
114
|
+
case 'routes':
|
|
115
|
+
require('../src/cli/routes')(args.slice(1));
|
|
116
|
+
break;
|
|
117
|
+
case 'lint':
|
|
118
|
+
require('../src/cli/lint')(args.slice(1));
|
|
119
|
+
break;
|
|
120
|
+
case 'test':
|
|
121
|
+
require('../src/cli/test')(args.slice(1));
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
console.error(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
125
|
+
console.log(`Run ${c.cyan}volt --help${c.reset} for usage information.`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
130
|
+
console.error(`${c.red}Error: Command module not found. Please reinstall VoltJS.${c.reset}`);
|
|
131
|
+
console.error(`${c.dim}${err.message}${c.reset}`);
|
|
132
|
+
} else {
|
|
133
|
+
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
|
134
|
+
if (process.env.VOLT_DEBUG) {
|
|
135
|
+
console.error(err.stack);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voltjs-framework",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "VoltJS - Lightning-fast, batteries-included, security-first JavaScript framework. Zero boilerplate, maximum power.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"volt": "./bin/volt.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node tests/run.js",
|
|
11
|
+
"dev": "node bin/volt.js dev",
|
|
12
|
+
"build": "node bin/volt.js build",
|
|
13
|
+
"lint": "node bin/volt.js lint"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"framework",
|
|
17
|
+
"voltjs",
|
|
18
|
+
"volt-framework",
|
|
19
|
+
"fullstack",
|
|
20
|
+
"ssr",
|
|
21
|
+
"react-ssr",
|
|
22
|
+
"security",
|
|
23
|
+
"batteries-included",
|
|
24
|
+
"minimal",
|
|
25
|
+
"reactive",
|
|
26
|
+
"web-framework",
|
|
27
|
+
"nodejs",
|
|
28
|
+
"rest-api",
|
|
29
|
+
"orm",
|
|
30
|
+
"authentication"
|
|
31
|
+
],
|
|
32
|
+
"author": "cool_phosphorus",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src/",
|
|
39
|
+
"bin/",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"react": "^19.2.4",
|
|
45
|
+
"react-dom": "^19.2.4",
|
|
46
|
+
"ws": "^8.16.0"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": ""
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://voltjs.dev",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": ""
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS GraphQL Handler
|
|
3
|
+
*
|
|
4
|
+
* Lightweight GraphQL execution engine — no external dependencies.
|
|
5
|
+
* Supports queries, mutations, variables, and introspection basics.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { GraphQLHandler } = require('voltjs');
|
|
9
|
+
*
|
|
10
|
+
* const gql = new GraphQLHandler();
|
|
11
|
+
*
|
|
12
|
+
* gql.type('User', {
|
|
13
|
+
* id: 'ID!',
|
|
14
|
+
* name: 'String!',
|
|
15
|
+
* email: 'String',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* gql.query('user', { id: 'ID!' }, async ({ id }) => {
|
|
19
|
+
* return await User.find(id);
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* gql.query('users', {}, async () => {
|
|
23
|
+
* return await User.all();
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* gql.mutation('createUser', { name: 'String!', email: 'String!' }, async (args) => {
|
|
27
|
+
* return await User.create(args);
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Mount as middleware
|
|
31
|
+
* app.post('/graphql', gql.middleware());
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
class GraphQLHandler {
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.types = new Map();
|
|
39
|
+
this.queries = new Map();
|
|
40
|
+
this.mutations = new Map();
|
|
41
|
+
this.subscriptions = new Map();
|
|
42
|
+
this.middleware_list = options.middleware || [];
|
|
43
|
+
this.maxDepth = options.maxDepth || 10;
|
|
44
|
+
this.introspection = options.introspection !== false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Define a type */
|
|
48
|
+
type(name, fields) {
|
|
49
|
+
this.types.set(name, fields);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Define a query resolver */
|
|
54
|
+
query(name, args, resolver) {
|
|
55
|
+
if (typeof args === 'function') {
|
|
56
|
+
resolver = args;
|
|
57
|
+
args = {};
|
|
58
|
+
}
|
|
59
|
+
this.queries.set(name, { args, resolver });
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Define a mutation resolver */
|
|
64
|
+
mutation(name, args, resolver) {
|
|
65
|
+
if (typeof args === 'function') {
|
|
66
|
+
resolver = args;
|
|
67
|
+
args = {};
|
|
68
|
+
}
|
|
69
|
+
this.mutations.set(name, { args, resolver });
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Define a subscription resolver */
|
|
74
|
+
subscription(name, args, resolver) {
|
|
75
|
+
if (typeof args === 'function') {
|
|
76
|
+
resolver = args;
|
|
77
|
+
args = {};
|
|
78
|
+
}
|
|
79
|
+
this.subscriptions.set(name, { args, resolver });
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Execute a GraphQL request */
|
|
84
|
+
async execute(request, context = {}) {
|
|
85
|
+
try {
|
|
86
|
+
const { query, variables = {}, operationName } = typeof request === 'string'
|
|
87
|
+
? { query: request }
|
|
88
|
+
: request;
|
|
89
|
+
|
|
90
|
+
if (!query) {
|
|
91
|
+
return { errors: [{ message: 'Query is required' }] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsed = this._parse(query);
|
|
95
|
+
|
|
96
|
+
if (!parsed || parsed.length === 0) {
|
|
97
|
+
return { errors: [{ message: 'Failed to parse query' }] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Find the operation to execute
|
|
101
|
+
let operation = parsed[0];
|
|
102
|
+
if (operationName) {
|
|
103
|
+
operation = parsed.find(op => op.name === operationName) || parsed[0];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const resolvers = operation.type === 'mutation' ? this.mutations : this.queries;
|
|
107
|
+
const data = {};
|
|
108
|
+
const errors = [];
|
|
109
|
+
|
|
110
|
+
for (const field of operation.fields) {
|
|
111
|
+
const resolver = resolvers.get(field.name);
|
|
112
|
+
|
|
113
|
+
// Introspection: __schema, __type
|
|
114
|
+
if (field.name === '__schema' && this.introspection) {
|
|
115
|
+
data.__schema = this._introspectSchema();
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (field.name === '__type' && this.introspection) {
|
|
119
|
+
const typeName = this._resolveArgs(field.args, variables).name;
|
|
120
|
+
data.__type = this._introspectType(typeName);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!resolver) {
|
|
125
|
+
errors.push({ message: `Unknown field: ${field.name}`, path: [field.name] });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const args = this._resolveArgs(field.args, variables);
|
|
131
|
+
const result = await resolver.resolver(args, context);
|
|
132
|
+
data[field.alias || field.name] = this._filterFields(result, field.fields);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
errors.push({
|
|
135
|
+
message: err.message,
|
|
136
|
+
path: [field.name],
|
|
137
|
+
extensions: { code: err.code || 'INTERNAL_ERROR' },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const response = { data };
|
|
143
|
+
if (errors.length > 0) response.errors = errors;
|
|
144
|
+
return response;
|
|
145
|
+
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
errors: [{ message: error.message || 'Internal Error' }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Express/Volt middleware */
|
|
154
|
+
middleware() {
|
|
155
|
+
return async (req, res) => {
|
|
156
|
+
// Only handle POST to the GraphQL endpoint
|
|
157
|
+
if (req.method === 'GET' && req.query?.query) {
|
|
158
|
+
const result = await this.execute({
|
|
159
|
+
query: req.query.query,
|
|
160
|
+
variables: req.query.variables ? JSON.parse(req.query.variables) : {},
|
|
161
|
+
operationName: req.query.operationName,
|
|
162
|
+
}, { req, res });
|
|
163
|
+
res.json(result);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (req.method === 'POST') {
|
|
168
|
+
const result = await this.execute(req.body, { req, res });
|
|
169
|
+
const status = result.errors && !result.data ? 400 : 200;
|
|
170
|
+
res.json(result, status);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Generate schema SDL string */
|
|
177
|
+
toSDL() {
|
|
178
|
+
const lines = [];
|
|
179
|
+
|
|
180
|
+
for (const [name, fields] of this.types) {
|
|
181
|
+
lines.push(`type ${name} {`);
|
|
182
|
+
for (const [field, type] of Object.entries(fields)) {
|
|
183
|
+
lines.push(` ${field}: ${type}`);
|
|
184
|
+
}
|
|
185
|
+
lines.push('}', '');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.queries.size > 0) {
|
|
189
|
+
lines.push('type Query {');
|
|
190
|
+
for (const [name, { args }] of this.queries) {
|
|
191
|
+
const argStr = Object.keys(args).length > 0
|
|
192
|
+
? `(${Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(', ')})`
|
|
193
|
+
: '';
|
|
194
|
+
lines.push(` ${name}${argStr}: JSON`);
|
|
195
|
+
}
|
|
196
|
+
lines.push('}', '');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (this.mutations.size > 0) {
|
|
200
|
+
lines.push('type Mutation {');
|
|
201
|
+
for (const [name, { args }] of this.mutations) {
|
|
202
|
+
const argStr = Object.keys(args).length > 0
|
|
203
|
+
? `(${Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(', ')})`
|
|
204
|
+
: '';
|
|
205
|
+
lines.push(` ${name}${argStr}: JSON`);
|
|
206
|
+
}
|
|
207
|
+
lines.push('}', '');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ===== PARSER =====
|
|
214
|
+
|
|
215
|
+
_parse(query) {
|
|
216
|
+
const operations = [];
|
|
217
|
+
const cleaned = query.replace(/#.*/g, '').trim();
|
|
218
|
+
|
|
219
|
+
// Match operation blocks: query/mutation Name { ... } or just { ... }
|
|
220
|
+
const opRegex = /(?:(query|mutation|subscription)\s*(\w*)\s*(?:\(([^)]*)\))?\s*)?{([\s\S]*?)}\s*$/gm;
|
|
221
|
+
|
|
222
|
+
// Simple top-level parse
|
|
223
|
+
let type = 'query';
|
|
224
|
+
let name = null;
|
|
225
|
+
let body = cleaned;
|
|
226
|
+
|
|
227
|
+
const opMatch = cleaned.match(/^(query|mutation|subscription)\s*(\w*)\s*(?:\(([^)]*)\))?\s*\{/);
|
|
228
|
+
if (opMatch) {
|
|
229
|
+
type = opMatch[1];
|
|
230
|
+
name = opMatch[2] || null;
|
|
231
|
+
body = cleaned.slice(opMatch[0].length - 1); // Keep opening {
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Remove outer braces
|
|
235
|
+
body = body.trim();
|
|
236
|
+
if (body.startsWith('{')) body = body.slice(1);
|
|
237
|
+
if (body.endsWith('}')) body = body.slice(0, -1);
|
|
238
|
+
|
|
239
|
+
const fields = this._parseFields(body);
|
|
240
|
+
|
|
241
|
+
operations.push({ type, name, fields });
|
|
242
|
+
return operations;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_parseFields(body) {
|
|
246
|
+
const fields = [];
|
|
247
|
+
const tokens = body.trim();
|
|
248
|
+
if (!tokens) return fields;
|
|
249
|
+
|
|
250
|
+
let i = 0;
|
|
251
|
+
while (i < tokens.length) {
|
|
252
|
+
// Skip whitespace
|
|
253
|
+
while (i < tokens.length && /\s/.test(tokens[i])) i++;
|
|
254
|
+
if (i >= tokens.length) break;
|
|
255
|
+
|
|
256
|
+
// Read field name (might have alias)
|
|
257
|
+
let fieldName = '';
|
|
258
|
+
let alias = null;
|
|
259
|
+
while (i < tokens.length && /[\w]/.test(tokens[i])) {
|
|
260
|
+
fieldName += tokens[i]; i++;
|
|
261
|
+
}
|
|
262
|
+
while (i < tokens.length && /\s/.test(tokens[i])) i++;
|
|
263
|
+
|
|
264
|
+
// Check for alias
|
|
265
|
+
if (tokens[i] === ':') {
|
|
266
|
+
alias = fieldName;
|
|
267
|
+
i++; // skip :
|
|
268
|
+
while (i < tokens.length && /\s/.test(tokens[i])) i++;
|
|
269
|
+
fieldName = '';
|
|
270
|
+
while (i < tokens.length && /[\w]/.test(tokens[i])) {
|
|
271
|
+
fieldName += tokens[i]; i++;
|
|
272
|
+
}
|
|
273
|
+
while (i < tokens.length && /\s/.test(tokens[i])) i++;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!fieldName) { i++; continue; }
|
|
277
|
+
|
|
278
|
+
// Parse args
|
|
279
|
+
let args = {};
|
|
280
|
+
if (tokens[i] === '(') {
|
|
281
|
+
const argEnd = this._findClosing(tokens, i, '(', ')');
|
|
282
|
+
const argStr = tokens.slice(i + 1, argEnd);
|
|
283
|
+
args = this._parseArgs(argStr);
|
|
284
|
+
i = argEnd + 1;
|
|
285
|
+
while (i < tokens.length && /\s/.test(tokens[i])) i++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Parse sub-fields
|
|
289
|
+
let subFields = null;
|
|
290
|
+
if (tokens[i] === '{') {
|
|
291
|
+
const blockEnd = this._findClosing(tokens, i, '{', '}');
|
|
292
|
+
const subBody = tokens.slice(i + 1, blockEnd);
|
|
293
|
+
subFields = this._parseFields(subBody);
|
|
294
|
+
i = blockEnd + 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fields.push({ name: fieldName, alias, args, fields: subFields });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return fields;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_parseArgs(argStr) {
|
|
304
|
+
const args = {};
|
|
305
|
+
const parts = argStr.split(',');
|
|
306
|
+
for (const part of parts) {
|
|
307
|
+
const colonIdx = part.indexOf(':');
|
|
308
|
+
if (colonIdx === -1) continue;
|
|
309
|
+
const key = part.slice(0, colonIdx).trim();
|
|
310
|
+
let value = part.slice(colonIdx + 1).trim();
|
|
311
|
+
|
|
312
|
+
// Parse value
|
|
313
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
314
|
+
value = value.slice(1, -1);
|
|
315
|
+
} else if (value.startsWith('$')) {
|
|
316
|
+
value = { $var: value.slice(1) };
|
|
317
|
+
} else if (value === 'true') {
|
|
318
|
+
value = true;
|
|
319
|
+
} else if (value === 'false') {
|
|
320
|
+
value = false;
|
|
321
|
+
} else if (value === 'null') {
|
|
322
|
+
value = null;
|
|
323
|
+
} else if (!isNaN(value)) {
|
|
324
|
+
value = Number(value);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
args[key] = value;
|
|
328
|
+
}
|
|
329
|
+
return args;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_resolveArgs(args, variables) {
|
|
333
|
+
const resolved = {};
|
|
334
|
+
for (const [key, value] of Object.entries(args)) {
|
|
335
|
+
if (value && typeof value === 'object' && value.$var) {
|
|
336
|
+
resolved[key] = variables[value.$var];
|
|
337
|
+
} else {
|
|
338
|
+
resolved[key] = value;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return resolved;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_filterFields(data, fields) {
|
|
345
|
+
if (!fields || !data) return data;
|
|
346
|
+
if (Array.isArray(data)) {
|
|
347
|
+
return data.map(item => this._filterFields(item, fields));
|
|
348
|
+
}
|
|
349
|
+
if (typeof data !== 'object') return data;
|
|
350
|
+
|
|
351
|
+
const result = {};
|
|
352
|
+
for (const field of fields) {
|
|
353
|
+
const key = field.name;
|
|
354
|
+
if (key in data) {
|
|
355
|
+
result[field.alias || key] = field.fields
|
|
356
|
+
? this._filterFields(data[key], field.fields)
|
|
357
|
+
: data[key];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_findClosing(str, start, open, close) {
|
|
364
|
+
let depth = 1;
|
|
365
|
+
let i = start + 1;
|
|
366
|
+
while (i < str.length && depth > 0) {
|
|
367
|
+
if (str[i] === open) depth++;
|
|
368
|
+
else if (str[i] === close) depth--;
|
|
369
|
+
if (depth > 0) i++;
|
|
370
|
+
}
|
|
371
|
+
return i;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ===== INTROSPECTION =====
|
|
375
|
+
|
|
376
|
+
_introspectSchema() {
|
|
377
|
+
return {
|
|
378
|
+
queryType: { name: 'Query' },
|
|
379
|
+
mutationType: this.mutations.size > 0 ? { name: 'Mutation' } : null,
|
|
380
|
+
types: [...this.types.keys()].map(name => this._introspectType(name)),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_introspectType(name) {
|
|
385
|
+
const fields = this.types.get(name);
|
|
386
|
+
if (!fields) return null;
|
|
387
|
+
return {
|
|
388
|
+
name,
|
|
389
|
+
kind: 'OBJECT',
|
|
390
|
+
fields: Object.entries(fields).map(([fieldName, type]) => ({
|
|
391
|
+
name: fieldName,
|
|
392
|
+
type: { name: type.replace('!', ''), kind: 'SCALAR', ofType: null },
|
|
393
|
+
isDeprecated: false,
|
|
394
|
+
})),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = { GraphQLHandler };
|