mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
refactor(dynamic): add GraphQL framework-specific fallbacks with Juniper, Relay, Apollo integration; enhancements for Prisma, Alembic, Channels, and ActionCable
This commit is contained in:
parent
1a0e2d204b
commit
ed8decb510
5 changed files with 566 additions and 5 deletions
|
|
@ -1156,15 +1156,135 @@ fn emit_graphql_resolver(
|
|||
) -> HarnessSource {
|
||||
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
|
||||
let handler = &spec.entry_name;
|
||||
let framework = spec
|
||||
.framework
|
||||
.as_ref()
|
||||
.map(|binding| binding.adapter.as_str())
|
||||
.unwrap_or("");
|
||||
let body = format!(
|
||||
r#"{preamble}
|
||||
// Phase 21 (Track M.3) — GraphQL resolver.
|
||||
process.stdout.write('__NYX_GRAPHQL_RESOLVER__: ' + {type_name:?} + '.' + {field:?} + '\n');
|
||||
const _nyxFramework = {framework:?};
|
||||
const _h = _nyxResolve({handler:?});
|
||||
if (_h == null) {{
|
||||
process.stderr.write('NYX_RESOLVER_NOT_FOUND: ' + {handler:?} + '\n');
|
||||
process.exit(78);
|
||||
}}
|
||||
async function _nyxTryGraphqlRelay(typeName, fieldName, resolver) {{
|
||||
let gql;
|
||||
let relay;
|
||||
try {{
|
||||
gql = require('graphql');
|
||||
relay = require('graphql-relay');
|
||||
}} catch (_) {{
|
||||
return false;
|
||||
}}
|
||||
const graphql = gql.graphql;
|
||||
const GraphQLSchema = gql.GraphQLSchema;
|
||||
const GraphQLObjectType = gql.GraphQLObjectType;
|
||||
const GraphQLString = gql.GraphQLString;
|
||||
const nodeDefinitions = relay.nodeDefinitions;
|
||||
const globalIdField = relay.globalIdField;
|
||||
const fromGlobalId = relay.fromGlobalId;
|
||||
const toGlobalId = relay.toGlobalId;
|
||||
if (typeof graphql !== 'function' || typeof nodeDefinitions !== 'function') return false;
|
||||
const safeField = /^[A-Za-z_][A-Za-z0-9_]*$/.test(fieldName) ? fieldName : 'nyxField';
|
||||
const safeType = /^[A-Za-z_][A-Za-z0-9_]*$/.test(typeName) ? typeName : 'NyxNode';
|
||||
const nodeTypeName = (safeType === 'Query' || safeType === 'Node') ? 'NyxRelayNode' : safeType;
|
||||
try {{
|
||||
let NodeType;
|
||||
const defs = nodeDefinitions(
|
||||
async function (globalId, context, info) {{
|
||||
let decoded = {{}};
|
||||
try {{ decoded = fromGlobalId(globalId) || {{}}; }} catch (_) {{}}
|
||||
const relayId = decoded.id || globalId || payload;
|
||||
const value = await Promise.resolve(resolver(
|
||||
null,
|
||||
{{ id: relayId, input: payload, value: payload, globalId }},
|
||||
context || {{}},
|
||||
info || {{ fieldName: safeField, parentType: nodeTypeName }}
|
||||
));
|
||||
return value == null ? {{ id: relayId }} : value;
|
||||
}},
|
||||
function () {{ return NodeType; }}
|
||||
);
|
||||
NodeType = new GraphQLObjectType({{
|
||||
name: nodeTypeName,
|
||||
interfaces: [defs.nodeInterface],
|
||||
fields: function () {{
|
||||
const fields = {{
|
||||
id: globalIdField(nodeTypeName, function (obj) {{
|
||||
return obj && obj.id != null ? String(obj.id) : String(payload);
|
||||
}}),
|
||||
}};
|
||||
fields[safeField] = {{
|
||||
type: GraphQLString,
|
||||
resolve: function (obj) {{
|
||||
if (obj == null) return null;
|
||||
const value = obj[safeField] != null ? obj[safeField]
|
||||
: (obj._sql != null ? obj._sql
|
||||
: (obj._query != null ? obj._query
|
||||
: (obj.name != null ? obj.name : obj.id)));
|
||||
return value == null ? null : String(value);
|
||||
}},
|
||||
}};
|
||||
return fields;
|
||||
}},
|
||||
}});
|
||||
const QueryType = new GraphQLObjectType({{
|
||||
name: 'Query',
|
||||
fields: function () {{
|
||||
const fields = {{ node: defs.nodeField }};
|
||||
fields[safeField] = {{
|
||||
type: GraphQLString,
|
||||
args: {{ id: {{ type: GraphQLString }}, input: {{ type: GraphQLString }} }},
|
||||
resolve: async function (_parent, args, context, info) {{
|
||||
const value = await Promise.resolve(resolver(
|
||||
null,
|
||||
Object.assign({{ id: payload, input: payload, value: payload }}, args || {{}}),
|
||||
context || {{}},
|
||||
info || {{ fieldName: safeField, parentType: safeType }}
|
||||
));
|
||||
if (value == null) return null;
|
||||
if (typeof value === 'object') {{
|
||||
const out = value[safeField] != null ? value[safeField]
|
||||
: (value._sql != null ? value._sql
|
||||
: (value._query != null ? value._query
|
||||
: (value.name != null ? value.name : value.id)));
|
||||
return out == null ? null : String(out);
|
||||
}}
|
||||
return String(value);
|
||||
}},
|
||||
}};
|
||||
return fields;
|
||||
}},
|
||||
}});
|
||||
const schema = new GraphQLSchema({{ query: QueryType, types: [NodeType] }});
|
||||
const globalId = toGlobalId(nodeTypeName, payload);
|
||||
const nodeSelection = safeField === 'id'
|
||||
? 'id'
|
||||
: 'id ... on ' + nodeTypeName + ' {{ ' + safeField + ' }}';
|
||||
const source = 'query($id: ID!, $value: String) {{ node(id: $id) {{ ' +
|
||||
nodeSelection + ' }} ' + safeField + '(id: $value, input: $value) }}';
|
||||
const result = await graphql({{
|
||||
schema,
|
||||
source,
|
||||
variableValues: {{ id: globalId, value: payload }},
|
||||
}});
|
||||
if (result.errors && result.errors.length) return false;
|
||||
if (result.data && result.data[safeField] != null) {{
|
||||
process.stdout.write(String(result.data[safeField]) + '\n');
|
||||
}}
|
||||
if (result.data && result.data.node && result.data.node[safeField] != null) {{
|
||||
process.stdout.write(String(result.data.node[safeField]) + '\n');
|
||||
}}
|
||||
return true;
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_GRAPHQL_RELAY_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||
return false;
|
||||
}}
|
||||
}}
|
||||
async function _nyxTryApolloServer(typeName, fieldName, resolver) {{
|
||||
let ApolloServer;
|
||||
let needsStart = true;
|
||||
|
|
@ -1257,6 +1377,7 @@ async function _nyxTryGraphqlJs(typeName, fieldName, resolver) {{
|
|||
}}
|
||||
(async () => {{
|
||||
try {{
|
||||
if (_nyxFramework === 'graphql-relay' && await _nyxTryGraphqlRelay({type_name:?}, {field:?}, _h)) return;
|
||||
if (await _nyxTryApolloServer({type_name:?}, {field:?}, _h)) return;
|
||||
if (await _nyxTryGraphqlJs({type_name:?}, {field:?}, _h)) return;
|
||||
// Apollo resolver shape: (parent, args, context, info).
|
||||
|
|
@ -1272,6 +1393,7 @@ async function _nyxTryGraphqlJs(typeName, fieldName, resolver) {{
|
|||
handler = handler,
|
||||
type_name = type_name,
|
||||
field = field,
|
||||
framework = framework,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
|
|
@ -1522,15 +1644,100 @@ const _qi = _realSequelize ? _realSequelize.queryInterface : {{
|
|||
sequelize: {{ query: async function(sql){{ _nyxMigrationSqlRecord(sql, 'sequelize'); return sql; }} }},
|
||||
}};
|
||||
const _sequelizeNamespace = _realSequelize ? _realSequelize.Sequelize : {{}};
|
||||
const _prisma = {{
|
||||
const _nyxPrismaShim = {{
|
||||
$executeRaw: async function(s){{ if (s) _nyxMigrationSqlRecord(s, 'prisma'); return s; }},
|
||||
$executeRawUnsafe: async function(s){{ if (s) {{ _nyxMigrationSqlRecord(s, 'prisma'); process.stdout.write('NYX_PRISMA_SQL: ' + s + '\n'); }} return s; }},
|
||||
$queryRaw: async function(s){{ if (s) _nyxMigrationSqlRecord(s, 'prisma'); return s; }},
|
||||
$queryRawUnsafe: async function(s){{ if (s) _nyxMigrationSqlRecord(s, 'prisma'); return s; }},
|
||||
}};
|
||||
let _realPrisma = null;
|
||||
let _prisma = _nyxPrismaShim;
|
||||
global.__nyx_prisma = _prisma;
|
||||
function _nyxPrismaSqlText(statement, values) {{
|
||||
if (Array.isArray(statement)) {{
|
||||
let out = '';
|
||||
for (let i = 0; i < statement.length; i++) {{
|
||||
out += String(statement[i]);
|
||||
if (i < values.length) out += String(values[i]);
|
||||
}}
|
||||
return out;
|
||||
}}
|
||||
if (statement && Array.isArray(statement.raw)) {{
|
||||
return _nyxPrismaSqlText(statement.raw, values);
|
||||
}}
|
||||
return String(statement || '');
|
||||
}}
|
||||
async function _nyxTryRealPrismaClient() {{
|
||||
try {{
|
||||
const PrismaPkg = require('@prisma/client');
|
||||
const PrismaClient = PrismaPkg.PrismaClient;
|
||||
if (typeof PrismaClient !== 'function') return null;
|
||||
const endpoint = process.env.NYX_SQL_ENDPOINT || '';
|
||||
if (endpoint && !process.env.DATABASE_URL) {{
|
||||
process.env.DATABASE_URL = 'file:' + endpoint;
|
||||
}}
|
||||
const client = new PrismaClient();
|
||||
const wrapped = Object.create(client);
|
||||
wrapped.$executeRaw = async function(statement, ...values) {{
|
||||
const sql = _nyxPrismaSqlText(statement, values);
|
||||
if (sql) _nyxMigrationSqlRecord(sql, 'prisma-client');
|
||||
try {{
|
||||
if (typeof client.$executeRawUnsafe === 'function') {{
|
||||
return await client.$executeRawUnsafe(sql);
|
||||
}}
|
||||
return await client.$executeRaw(statement, ...values);
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_PRISMA_CLIENT_EXEC_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||
return sql;
|
||||
}}
|
||||
}};
|
||||
wrapped.$executeRawUnsafe = async function(statement, ...values) {{
|
||||
const sql = _nyxPrismaSqlText(statement, values);
|
||||
if (sql) {{
|
||||
_nyxMigrationSqlRecord(sql, 'prisma-client');
|
||||
process.stdout.write('NYX_PRISMA_CLIENT_SQL: ' + sql + '\n');
|
||||
}}
|
||||
try {{
|
||||
return await client.$executeRawUnsafe(statement, ...values);
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_PRISMA_CLIENT_EXEC_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||
return sql;
|
||||
}}
|
||||
}};
|
||||
wrapped.$queryRaw = async function(statement, ...values) {{
|
||||
const sql = _nyxPrismaSqlText(statement, values);
|
||||
if (sql) _nyxMigrationSqlRecord(sql, 'prisma-client');
|
||||
try {{
|
||||
return await client.$queryRaw(statement, ...values);
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_PRISMA_CLIENT_QUERY_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||
return sql;
|
||||
}}
|
||||
}};
|
||||
wrapped.$queryRawUnsafe = async function(statement, ...values) {{
|
||||
const sql = _nyxPrismaSqlText(statement, values);
|
||||
if (sql) _nyxMigrationSqlRecord(sql, 'prisma-client');
|
||||
try {{
|
||||
return await client.$queryRawUnsafe(statement, ...values);
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_PRISMA_CLIENT_QUERY_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||
return sql;
|
||||
}}
|
||||
}};
|
||||
return {{ client, prisma: wrapped }};
|
||||
}} catch (e) {{
|
||||
process.stderr.write('NYX_PRISMA_CLIENT_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||
return null;
|
||||
}}
|
||||
}}
|
||||
(async () => {{
|
||||
try {{
|
||||
_realPrisma = await _nyxTryRealPrismaClient();
|
||||
if (_realPrisma && _realPrisma.prisma) {{
|
||||
_prisma = _realPrisma.prisma;
|
||||
global.__nyx_prisma = _prisma;
|
||||
process.stdout.write('NYX_PRISMA_CLIENT=1\n');
|
||||
}}
|
||||
let _result;
|
||||
// Sequelize migrations conventionally take (queryInterface, Sequelize).
|
||||
// Single-arg migrations are Prisma/raw shapes and should receive payload.
|
||||
|
|
@ -1553,6 +1760,9 @@ global.__nyx_prisma = _prisma;
|
|||
process.stderr.write('NYX_EXCEPTION: ' + (e.constructor ? e.constructor.name : 'Error') + ': ' + e.message + '\n');
|
||||
}} finally {{
|
||||
if (_realSequelize && _realSequelize.close) await _realSequelize.close();
|
||||
if (_realPrisma && _realPrisma.client && typeof _realPrisma.client.$disconnect === 'function') {{
|
||||
try {{ await _realPrisma.client.$disconnect(); }} catch (e) {{}}
|
||||
}}
|
||||
}}
|
||||
}})();
|
||||
"#,
|
||||
|
|
|
|||
|
|
@ -1679,6 +1679,49 @@ if _h is None:
|
|||
print("NYX_HANDLER_NOT_FOUND: " + {handler:?}, file=sys.stderr, flush=True)
|
||||
sys.exit(78)
|
||||
try:
|
||||
def _nyx_try_channels(handler_name, body, ws_path):
|
||||
try:
|
||||
import asyncio
|
||||
from channels.testing import WebsocketCommunicator
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _nyx_find_consumer():
|
||||
for value in vars(_entry_mod).values():
|
||||
if isinstance(value, type) and (
|
||||
hasattr(value, "as_asgi") or hasattr(value, "receive")
|
||||
):
|
||||
if value.__name__.lower().endswith(("consumer", "websocket")):
|
||||
return value
|
||||
return None
|
||||
|
||||
async def _nyx_drive_consumer():
|
||||
consumer_cls = _nyx_find_consumer()
|
||||
if consumer_cls is None or not hasattr(consumer_cls, "as_asgi"):
|
||||
return False
|
||||
communicator = WebsocketCommunicator(consumer_cls.as_asgi(), ws_path or "/ws/")
|
||||
connected = False
|
||||
try:
|
||||
connected, _subprotocol = await communicator.connect()
|
||||
if not connected:
|
||||
return False
|
||||
await communicator.send_to(text_data=body)
|
||||
return True
|
||||
finally:
|
||||
if connected:
|
||||
try:
|
||||
await communicator.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return bool(asyncio.run(_nyx_drive_consumer()))
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as _e:
|
||||
print(f"NYX_CHANNELS_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
def _nyx_try_socketio(handler_name, handler, body):
|
||||
try:
|
||||
import socketio
|
||||
|
|
@ -1697,7 +1740,7 @@ try:
|
|||
print(f"NYX_SOCKETIO_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
if not _nyx_try_socketio({handler:?}, _h, payload):
|
||||
if not _nyx_try_channels({handler:?}, payload, {path:?}) and not _nyx_try_socketio({handler:?}, _h, payload):
|
||||
# python-socketio handlers are `def message(sid, data)`; Channels
|
||||
# consumers are `def receive(self, text_data=None, bytes_data=None)`.
|
||||
# Try (sid, payload) first, then fall back to (payload).
|
||||
|
|
@ -1938,6 +1981,94 @@ def _nyx_install_migration_sql_hooks():
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
def _nyx_try_alembic_command_upgrade():
|
||||
try:
|
||||
import tempfile
|
||||
import textwrap
|
||||
import types
|
||||
from pathlib import Path
|
||||
import alembic.command
|
||||
from alembic.config import Config
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
handler_name = {handler:?}
|
||||
if handler_name not in ("upgrade", "up"):
|
||||
return False
|
||||
|
||||
endpoint = os.environ.get("NYX_SQL_ENDPOINT", "")
|
||||
url = "sqlite:///" + endpoint if endpoint else "sqlite:///:memory:"
|
||||
root = Path(tempfile.mkdtemp(prefix="nyx-alembic-"))
|
||||
versions = root / "versions"
|
||||
versions.mkdir(parents=True, exist_ok=True)
|
||||
(root / "script.py.mako").write_text(
|
||||
"${{up_revision}} = ${{repr(up_revision)}}\n"
|
||||
"${{down_revision}} = ${{repr(down_revision)}}\n"
|
||||
"def upgrade():\n pass\n"
|
||||
"def downgrade():\n pass\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "env.py").write_text(textwrap.dedent("""
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
|
||||
target_metadata = None
|
||||
|
||||
def run_migrations_online():
|
||||
cfg = context.config.get_section(context.config.config_ini_section)
|
||||
connectable = engine_from_config(cfg, prefix="sqlalchemy.", poolclass=pool.NullPool)
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
run_migrations_online()
|
||||
"""), encoding="utf-8")
|
||||
|
||||
hooks = types.ModuleType("nyx_alembic_hooks")
|
||||
hooks.payload = payload
|
||||
hooks.load_entry = lambda: _entry_mod
|
||||
hooks.op_proxy = lambda inner=None: _NyxMigrationOpProxy(inner)
|
||||
hooks.record_result = _nyx_record_migration_result
|
||||
sys.modules["nyx_alembic_hooks"] = hooks
|
||||
|
||||
(versions / "0001_nyx_entry.py").write_text(textwrap.dedent("""
|
||||
revision = "0001_nyx_entry"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
def upgrade():
|
||||
from alembic import op as real_op
|
||||
from nyx_alembic_hooks import load_entry, op_proxy, payload, record_result
|
||||
mod = load_entry()
|
||||
if hasattr(mod, "op"):
|
||||
mod.op = op_proxy(real_op)
|
||||
fn = getattr(mod, "upgrade", None) or getattr(mod, "up", None)
|
||||
if fn is None:
|
||||
return
|
||||
try:
|
||||
result = fn(payload)
|
||||
except TypeError:
|
||||
result = fn()
|
||||
record_result(result)
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
"""), encoding="utf-8")
|
||||
|
||||
cfg = Config()
|
||||
cfg.set_main_option("script_location", str(root))
|
||||
cfg.set_main_option("sqlalchemy.url", url)
|
||||
try:
|
||||
alembic.command.upgrade(cfg, "head")
|
||||
return True
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as _e:
|
||||
print(f"NYX_ALEMBIC_COMMAND_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
|
||||
return False
|
||||
|
||||
def _nyx_record_migration_result(result):
|
||||
if result is None:
|
||||
return
|
||||
|
|
@ -2000,8 +2131,11 @@ def _nyx_run_django_migration_operations(cls):
|
|||
|
||||
try:
|
||||
_nyx_install_migration_sql_hooks()
|
||||
_ran_alembic_command = _nyx_try_alembic_command_upgrade()
|
||||
_result = None
|
||||
if _h is _migration_cls or ({handler:?} == "Migration" and _migration_cls is not None):
|
||||
if _ran_alembic_command:
|
||||
_nyx_run_django_migration_operations(_migration_cls)
|
||||
elif _h is _migration_cls or ({handler:?} == "Migration" and _migration_cls is not None):
|
||||
_nyx_run_django_migration_operations(_migration_cls)
|
||||
else:
|
||||
# Migrations conventionally take no arguments; pass payload if the
|
||||
|
|
|
|||
|
|
@ -818,10 +818,50 @@ fn emit_websocket_handler_harness(spec: &HarnessSpec, path: &str) -> HarnessSour
|
|||
r#"{preamble}
|
||||
puts "__NYX_WEBSOCKET__: " + {path:?}
|
||||
|
||||
def nyx_try_action_cable_channel(cls)
|
||||
begin
|
||||
require 'action_cable/channel/base'
|
||||
require 'logger'
|
||||
rescue LoadError
|
||||
return false
|
||||
end
|
||||
return false unless defined?(ActionCable::Channel::Base)
|
||||
return false unless cls.is_a?(Class) && cls < ActionCable::Channel::Base
|
||||
|
||||
begin
|
||||
connection = Object.new
|
||||
def connection.transmit(data)
|
||||
print(data.to_s) if data
|
||||
end
|
||||
def connection.logger
|
||||
@logger ||= Logger.new(IO::NULL)
|
||||
end
|
||||
def connection.identifiers
|
||||
[]
|
||||
end
|
||||
def connection.connection_identifier
|
||||
'nyx-action-cable'
|
||||
end
|
||||
channel = cls.new(connection, {{ 'channel' => cls.name }}, {{ 'nyx_payload' => $nyx_payload }})
|
||||
if channel.respond_to?(:perform_action)
|
||||
channel.perform_action({{ 'action' => 'receive', 'data' => $nyx_payload }})
|
||||
elsif channel.respond_to?(:receive)
|
||||
channel.receive($nyx_payload)
|
||||
else
|
||||
return false
|
||||
end
|
||||
true
|
||||
rescue StandardError => e
|
||||
STDERR.puts("NYX_ACTION_CABLE_FALLBACK: #{{e.class.name}}: #{{e.message}}") if ENV['NYX_DEBUG']
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# ActionCable channels expose `receive(data)` on a subclass. Find the
|
||||
# enclosing class via const lookup; fall back to top-level send.
|
||||
if Object.const_defined?({handler:?})
|
||||
cls = Object.const_get({handler:?})
|
||||
exit 0 if nyx_try_action_cable_channel(cls)
|
||||
begin
|
||||
inst = cls.new rescue (cls.allocate rescue nil)
|
||||
if inst && inst.respond_to?(:receive)
|
||||
|
|
|
|||
|
|
@ -2365,16 +2365,75 @@ fn emit_graphql_resolver_harness(
|
|||
let cargo_toml = generate_cargo_toml_for_spec(spec.expected_cap, RustShape::Generic, spec);
|
||||
let handler = &spec.entry_name;
|
||||
let label = format!("{type_name}.{field}");
|
||||
let use_juniper = spec
|
||||
.framework
|
||||
.as_ref()
|
||||
.map(|binding| binding.adapter.as_str() == "graphql-juniper")
|
||||
.unwrap_or(false);
|
||||
let field_ident = safe_rust_graphql_field_ident(field);
|
||||
let (juniper_driver, resolver_call) = if use_juniper {
|
||||
(
|
||||
format!(
|
||||
r#"
|
||||
struct NyxQueryRoot;
|
||||
|
||||
#[juniper::graphql_object]
|
||||
impl NyxQueryRoot {{
|
||||
fn {field_ident}(id: String) -> String {{
|
||||
entry::{handler}(&id)
|
||||
}}
|
||||
}}
|
||||
|
||||
fn nyx_try_juniper(payload: &str) -> bool {{
|
||||
let ctx = ();
|
||||
let schema = juniper::RootNode::new(
|
||||
NyxQueryRoot,
|
||||
juniper::EmptyMutation::<()>::new(),
|
||||
juniper::EmptySubscription::<()>::new(),
|
||||
);
|
||||
let mut vars = juniper::Variables::new();
|
||||
vars.insert("id".to_string(), juniper::InputValue::scalar(payload.to_string()));
|
||||
let query = "query Nyx($id: String!) {{ {field_ident}(id: $id) }}";
|
||||
match juniper::execute_sync(query, None, &schema, &vars, &ctx) {{
|
||||
Ok((value, errors)) if errors.is_empty() => {{
|
||||
println!("{{:?}}", value);
|
||||
true
|
||||
}}
|
||||
Ok((_value, errors)) => {{
|
||||
eprintln!("NYX_JUNIPER_ERRORS: {{:?}}", errors);
|
||||
false
|
||||
}}
|
||||
Err(err) => {{
|
||||
eprintln!("NYX_JUNIPER_FALLBACK: {{err:?}}");
|
||||
false
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
field_ident = field_ident,
|
||||
handler = handler,
|
||||
),
|
||||
" if !nyx_try_juniper(&payload) {\n let _ = entry::".to_owned()
|
||||
+ handler
|
||||
+ "(&payload);\n }\n",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
String::new(),
|
||||
" let _ = entry::".to_owned() + handler + "(&payload);\n",
|
||||
)
|
||||
};
|
||||
let body = format!(
|
||||
r#"//! Nyx dynamic harness — GraphQL resolver (Phase 21 / Track M.3).
|
||||
mod entry;
|
||||
{shim}
|
||||
{juniper_driver}
|
||||
fn main() {{
|
||||
let payload = nyx_payload();
|
||||
__nyx_install_crash_guard("{label}");
|
||||
println!("__NYX_GRAPHQL_RESOLVER__: {type_name}.{field}");
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = entry::{handler}(&payload);
|
||||
{resolver_call}
|
||||
}}
|
||||
|
||||
fn nyx_payload() -> String {{
|
||||
|
|
@ -2419,10 +2478,11 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
|
|||
Some(out)
|
||||
}}
|
||||
"#,
|
||||
handler = handler,
|
||||
type_name = type_name,
|
||||
field = field,
|
||||
label = label,
|
||||
juniper_driver = juniper_driver,
|
||||
resolver_call = resolver_call,
|
||||
);
|
||||
HarnessSource {
|
||||
source: body,
|
||||
|
|
@ -2433,6 +2493,60 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
|
|||
}
|
||||
}
|
||||
|
||||
fn safe_rust_graphql_field_ident(field: &str) -> String {
|
||||
let mut out = String::with_capacity(field.len().max(8));
|
||||
for ch in field.chars() {
|
||||
if ch.is_ascii_alphanumeric() || ch == '_' {
|
||||
out.push(ch);
|
||||
} else {
|
||||
out.push('_');
|
||||
}
|
||||
}
|
||||
if out.is_empty()
|
||||
|| out.as_bytes().first().is_some_and(|b| b.is_ascii_digit())
|
||||
|| matches!(
|
||||
out.as_str(),
|
||||
"as" | "break"
|
||||
| "const"
|
||||
| "continue"
|
||||
| "crate"
|
||||
| "else"
|
||||
| "enum"
|
||||
| "extern"
|
||||
| "false"
|
||||
| "fn"
|
||||
| "for"
|
||||
| "if"
|
||||
| "impl"
|
||||
| "in"
|
||||
| "let"
|
||||
| "loop"
|
||||
| "match"
|
||||
| "mod"
|
||||
| "move"
|
||||
| "mut"
|
||||
| "pub"
|
||||
| "ref"
|
||||
| "return"
|
||||
| "self"
|
||||
| "Self"
|
||||
| "static"
|
||||
| "struct"
|
||||
| "super"
|
||||
| "trait"
|
||||
| "true"
|
||||
| "type"
|
||||
| "unsafe"
|
||||
| "use"
|
||||
| "where"
|
||||
| "while"
|
||||
)
|
||||
{
|
||||
out.insert_str(0, "nyx_");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// True when the entry source declares `class` as a type that derives
|
||||
/// or implements `Default`. Two byte-level patterns are recognised:
|
||||
///
|
||||
|
|
|
|||
|
|
@ -781,6 +781,34 @@ fn graphql_resolver_js_apollo_stages_runtime_deps() {
|
|||
assert!(package.contains("\"graphql\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphql_resolver_js_relay_harness_uses_relay_runtime() {
|
||||
let spec = framework_bound_spec(
|
||||
Lang::JavaScript,
|
||||
EvEntryKind::GraphQLResolver {
|
||||
type_name: "Node".into(),
|
||||
field: "resolveNode".into(),
|
||||
},
|
||||
"resolveNode",
|
||||
"tests/dynamic_fixtures/graphql_resolver/relay/vuln.js",
|
||||
"graphql-relay",
|
||||
);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("_nyxTryGraphqlRelay"));
|
||||
assert!(h.source.contains("require('graphql-relay')"));
|
||||
assert!(h.source.contains("nodeDefinitions"));
|
||||
assert!(h.source.contains("toGlobalId"));
|
||||
assert!(h.source.contains("_nyxFramework === 'graphql-relay'"));
|
||||
assert!(
|
||||
h.source.find("_nyxTryGraphqlRelay").unwrap()
|
||||
< h.source.find("_nyxTryApolloServer").unwrap(),
|
||||
"Relay runtime should be attempted before the generic Apollo path for graphql-relay specs",
|
||||
);
|
||||
let package = extra_file_content(&h.extra_files, "package.json");
|
||||
assert!(package.contains("\"graphql-relay\""));
|
||||
assert!(package.contains("\"graphql\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphql_resolver_rust_harness_carries_sentinel_and_field() {
|
||||
let spec = make_spec(
|
||||
|
|
@ -797,6 +825,27 @@ fn graphql_resolver_rust_harness_carries_sentinel_and_field() {
|
|||
assert!(h.source.contains("entry::resolve_user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphql_resolver_rust_juniper_harness_uses_execute_sync() {
|
||||
let spec = framework_bound_spec(
|
||||
Lang::Rust,
|
||||
EvEntryKind::GraphQLResolver {
|
||||
type_name: "Query".into(),
|
||||
field: "user".into(),
|
||||
},
|
||||
"resolve_user",
|
||||
"tests/dynamic_fixtures/graphql_resolver/juniper/vuln.rs",
|
||||
"graphql-juniper",
|
||||
);
|
||||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("juniper::RootNode::new"));
|
||||
assert!(h.source.contains("juniper::execute_sync"));
|
||||
assert!(h.source.contains("fn user(id: String) -> String"));
|
||||
assert!(h.source.contains("if !nyx_try_juniper(&payload)"));
|
||||
let cargo = extra_file_content(&h.extra_files, "Cargo.toml");
|
||||
assert!(cargo.contains("juniper = \"0.16\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphql_resolver_go_harness_carries_sentinel_and_field() {
|
||||
let spec = make_spec(
|
||||
|
|
@ -828,6 +877,9 @@ fn websocket_python_harness_carries_sentinel_and_handler() {
|
|||
assert!(h.source.contains("__NYX_WEBSOCKET__"));
|
||||
assert!(h.source.contains("\"message\""));
|
||||
assert!(h.source.contains("/ws/chat"));
|
||||
assert!(h.source.contains("_nyx_try_channels"));
|
||||
assert!(h.source.contains("WebsocketCommunicator"));
|
||||
assert!(h.source.contains("as_asgi"));
|
||||
assert!(h.source.contains("_nyx_try_socketio"));
|
||||
assert!(h.source.contains("socketio.Server"));
|
||||
}
|
||||
|
|
@ -862,6 +914,9 @@ fn websocket_ruby_harness_carries_sentinel_and_handler() {
|
|||
let h = lang::emit(&spec).expect("emit ok");
|
||||
assert!(h.source.contains("__NYX_WEBSOCKET__"));
|
||||
assert!(h.source.contains("ChatChannel"));
|
||||
assert!(h.source.contains("nyx_try_action_cable_channel"));
|
||||
assert!(h.source.contains("ActionCable::Channel::Base"));
|
||||
assert!(h.source.contains("perform_action"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -962,6 +1017,10 @@ fn migration_python_harness_carries_sentinel_and_handler() {
|
|||
assert!(h.source.contains("\"upgrade\""));
|
||||
assert!(h.source.contains("__nyx_stub_sql_record"));
|
||||
assert!(h.source.contains("MigrationContext.configure"));
|
||||
assert!(h.source.contains("_nyx_try_alembic_command_upgrade"));
|
||||
assert!(h.source.contains("alembic.command.upgrade"));
|
||||
assert!(h.source.contains("script_location"));
|
||||
assert!(h.source.contains("nyx_alembic_hooks"));
|
||||
assert!(h.source.contains("NYX_SQL_ENDPOINT"));
|
||||
assert!(h.source.contains("def create_table"));
|
||||
assert!(h.source.contains("def add_column"));
|
||||
|
|
@ -983,6 +1042,10 @@ fn migration_js_harness_carries_sentinel_and_handler() {
|
|||
assert!(h.source.contains("require('sequelize')"));
|
||||
assert!(h.source.contains("getQueryInterface"));
|
||||
assert!(h.source.contains("global.__nyx_prisma"));
|
||||
assert!(h.source.contains("require('@prisma/client')"));
|
||||
assert!(h.source.contains("_nyxTryRealPrismaClient"));
|
||||
assert!(h.source.contains("NYX_PRISMA_CLIENT_SQL"));
|
||||
assert!(h.source.contains("$disconnect"));
|
||||
assert!(h.source.contains("node:sqlite"));
|
||||
assert!(h.source.contains("NYX_SQL_ENDPOINT"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue