Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions lib/core/extensions/extension_support.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,43 @@ import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:querya_desktop/core/extensions/models/extension_manifest.dart';
import 'package:querya_desktop/core/extensions/models/extension_type.dart';
import 'package:querya_desktop/core/extensions/models/sandbox_capabilities.dart';
import 'package:querya_desktop/core/extensions/sandbox/sandbox_policy.dart';
import 'package:querya_desktop/core/market/marketplace_repository.dart';

/// Support matrix for locally installed / marketplace extensions.
///
/// Themes install and apply fully. Database drivers are catalog preview until
/// the dynamic plugin runtime ships (see Marketplace API roadmap).
/// Themes and Level-1 scripts install fully. Database drivers remain preview
/// listings until they declare a policy-compliant Level-2 `sandbox.engine:
/// process` block (Block E M3).
class ExtensionSupport {
ExtensionSupport._();

static const databaseDriverPreviewNotice =
'Database drivers in the Marketplace are preview listings only. '
'Database drivers in the Marketplace are preview listings only until they '
'declare a policy-compliant OS process sandbox. '
'Querya connects using built-in Dart drivers (PostgreSQL, MySQL, SQLite, '
'Redis, MongoDB). External driver plugins will load via the Marketplace '
'plugin runtime in a future release.';
'Redis, MongoDB). Sandboxed external drivers install when '
'`sandbox.engine` is `process` and passes SandboxPolicy.';

static const databaseDriverMissingEntryMessage =
'Driver package is missing its main entry file. Installation aborted.';

/// Type-level preview heuristic (drivers default to preview).
/// Prefer [isPreviewOnlyManifest] when a full manifest is available.
static bool isPreviewOnly(ExtensionType type) =>
type == ExtensionType.databaseDriver;

static bool isPreviewOnlyManifest(ExtensionManifest manifest) =>
isPreviewOnly(manifest.type);
/// Drivers without a valid Level-2 process sandbox stay preview-only.
/// Scripts / themes are always installable (subject to SandboxPolicy).
static bool isPreviewOnlyManifest(ExtensionManifest manifest) {
if (manifest.type != ExtensionType.databaseDriver) return false;
final sandbox = manifest.sandbox;
if (sandbox == null || sandbox.engine != SandboxEngine.process) {
return true;
}
return !SandboxPolicy.isAllowed(manifest);
}

/// Ensures a database driver archive contains the declared [ExtensionManifest.main].
static void validateDriverPackage({
Expand Down
3 changes: 3 additions & 0 deletions lib/core/extensions/models/extension_type.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
enum ExtensionType {
databaseDriver('database_driver'),
theme('theme'),

/// Level-1 embedded scripts: SDUI transformers, SQL formatters, parsers.
script('script'),
unknown('unknown');

final String value;
Expand Down
239 changes: 239 additions & 0 deletions lib/core/extensions/sandbox/embedded/declarative_embedded_engine.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import 'dart:convert';

import 'package:querya_desktop/core/extensions/models/sandbox_capabilities.dart';
import 'package:querya_desktop/core/extensions/sandbox/embedded/embedded_sandbox_engine.dart';

/// Pure-Dart Level-1 engine for JSON / JSONC declarative modules.
///
/// Guests declare transforms in data form — no eval, no network, no filesystem.
/// Suitable for SDUI transformers, SQL formatters, and hint generators until
/// QuickJS / WASM FFI backends are linked.
class DeclarativeEmbeddedEngine implements EmbeddedSandboxEngine {
@override
SandboxEngine get engine => SandboxEngine.quickjs; // logical Level-1 slot

/// Alternate id when the module explicitly targets wasm-shaped JSON modules.
final bool treatAsWasm;

DeclarativeEmbeddedEngine({this.treatAsWasm = false});

@override
bool get isAvailable => true;

@override
Future<void> dispose() async {}

@override
Future<EmbeddedInvokeResult> invoke(EmbeddedInvokeRequest request) async {
final source = request.source;
if (source == null || source.trim().isEmpty) {
return EmbeddedInvokeResult.failure('Module source is empty.');
}

late final Map<String, dynamic> module;
try {
module = _parseJsoncObject(source);
} catch (e) {
return EmbeddedInvokeResult.failure('Invalid declarative module: $e');
}

final kind = module['kind'] as String? ?? 'pipeline';
switch (request.method) {
case EmbeddedInvokeMethod.sduiTransform:
return _sduiTransform(module, request.args);
case EmbeddedInvokeMethod.sqlFormat:
return _sqlFormat(module, request.args);
case EmbeddedInvokeMethod.hintsGenerate:
return _hintsGenerate(module, request.args);
case EmbeddedInvokeMethod.sqlParse:
return _sqlParse(module, request.args);
case EmbeddedInvokeMethod.invoke:
return _dispatchByKind(kind, module, request);
}
}

EmbeddedInvokeResult _dispatchByKind(
String kind,
Map<String, dynamic> module,
EmbeddedInvokeRequest request,
) {
switch (kind) {
case 'sdui.transform':
case 'sdui':
return _sduiTransform(module, request.args);
case 'sql.format':
case 'sql_format':
return _sqlFormat(module, request.args);
case 'hints.generate':
case 'hints':
return _hintsGenerate(module, request.args);
case 'sql.parse':
case 'sql_parse':
return _sqlParse(module, request.args);
default:
return EmbeddedInvokeResult.failure('Unknown module kind "$kind".');
}
}

EmbeddedInvokeResult _sduiTransform(
Map<String, dynamic> module,
Map<String, Object?> args,
) {
final input = args['document'];
if (input is! Map) {
return EmbeddedInvokeResult.failure(
'sdui.transform requires args.document as a JSON object.',
);
}
final doc = Map<String, Object?>.from(
input.map((k, v) => MapEntry('$k', v)),
);

final renames = module['renameKeys'];
if (renames is Map) {
for (final entry in renames.entries) {
final from = '${entry.key}';
final to = '${entry.value}';
if (doc.containsKey(from)) {
doc[to] = doc.remove(from);
}
}
}

final defaults = module['defaults'];
if (defaults is Map) {
for (final entry in defaults.entries) {
doc.putIfAbsent('${entry.key}', () => entry.value);
}
}

final drop = module['dropKeys'];
if (drop is List) {
for (final key in drop) {
doc.remove('$key');
}
}

return EmbeddedInvokeResult.success(doc);
}

EmbeddedInvokeResult _sqlFormat(
Map<String, dynamic> module,
Map<String, Object?> args,
) {
final sql = args['sql'];
if (sql is! String) {
return EmbeddedInvokeResult.failure('sql.format requires args.sql string.');
}

var out = sql.replaceAll(RegExp(r'[ \t]+'), ' ').trim();
out = out.replaceAll(RegExp(r'\s*;\s*'), ';\n');
final upperKeywords = module['uppercaseKeywords'] != false;
if (upperKeywords) {
const keywords = [
'select',
'from',
'where',
'and',
'or',
'join',
'left',
'right',
'inner',
'outer',
'on',
'group',
'by',
'order',
'limit',
'insert',
'into',
'values',
'update',
'set',
'delete',
];
for (final kw in keywords) {
out = out.replaceAllMapped(
RegExp('\\b$kw\\b', caseSensitive: false),
(m) => kw.toUpperCase(),
);
}
}
return EmbeddedInvokeResult.success(out);
}

EmbeddedInvokeResult _hintsGenerate(
Map<String, dynamic> module,
Map<String, Object?> args,
) {
final tables = module['tables'];
if (tables is! List) {
return EmbeddedInvokeResult.failure(
'hints.generate module requires "tables" array.',
);
}
final prefix = (args['prefix'] as String? ?? '').toLowerCase();
final hints = <Map<String, Object?>>[];
for (final table in tables) {
if (table is! Map) continue;
final name = '${table['name'] ?? ''}';
if (name.isEmpty) continue;
if (prefix.isNotEmpty && !name.toLowerCase().startsWith(prefix)) {
continue;
}
hints.add({
'label': name,
'kind': 'table',
'detail': table['detail'],
});
final columns = table['columns'];
if (columns is List) {
for (final col in columns) {
final colName = '$col';
if (prefix.isNotEmpty &&
!colName.toLowerCase().startsWith(prefix) &&
!'$name.$colName'.toLowerCase().startsWith(prefix)) {
continue;
}
hints.add({
'label': colName,
'kind': 'column',
'detail': name,
});
}
}
}
return EmbeddedInvokeResult.success(hints);
}

EmbeddedInvokeResult _sqlParse(
Map<String, dynamic> module,
Map<String, Object?> args,
) {
final sql = args['sql'];
if (sql is! String) {
return EmbeddedInvokeResult.failure('sql.parse requires args.sql string.');
}
final trimmed = sql.trim();
final first = trimmed.split(RegExp(r'\s+')).first.toUpperCase();
final dialect = module['dialect'] as String? ?? 'generic';
return EmbeddedInvokeResult.success({
'dialect': dialect,
'statement': first,
'length': trimmed.length,
'raw': trimmed,
});
}

/// Strips `//` and `/* */` comments then `jsonDecode`s an object.
static Map<String, dynamic> _parseJsoncObject(String source) {
final withoutBlock = source.replaceAll(RegExp(r'/\*[\s\S]*?\*/'), '');
final withoutLine = withoutBlock.replaceAll(RegExp(r'//[^\n]*'), '');
final decoded = jsonDecode(withoutLine);
if (decoded is! Map<String, dynamic>) {
throw const FormatException('Module root must be a JSON object.');
}
return decoded;
}
}
83 changes: 83 additions & 0 deletions lib/core/extensions/sandbox/embedded/embedded_sandbox_engine.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:querya_desktop/core/extensions/models/sandbox_capabilities.dart';

/// Methods exposed to Level-1 embedded guests (no network / no host FS).
enum EmbeddedInvokeMethod {
/// Transform a Server-Driven UI JSON document.
sduiTransform('sdui.transform'),

/// Pretty-print / normalize SQL text.
sqlFormat('sql.format'),

/// Produce editor hint / autocomplete schema fragments.
hintsGenerate('hints.generate'),

/// Parse a dialect-specific SQL fragment into a JSON AST-ish structure.
sqlParse('sql.parse'),

/// Generic module entry (`main` / custom).
invoke('invoke');

const EmbeddedInvokeMethod(this.value);
final String value;

static EmbeddedInvokeMethod fromString(String value) {
return EmbeddedInvokeMethod.values.firstWhere(
(m) => m.value == value,
orElse: () => EmbeddedInvokeMethod.invoke,
);
}
}

class EmbeddedInvokeRequest {
const EmbeddedInvokeRequest({
required this.method,
this.args = const {},
this.source,
this.moduleId,
});

final EmbeddedInvokeMethod method;
final Map<String, Object?> args;

/// Already-loaded module source (JSON / JS / WASM bytes as base64, etc.).
final String? source;
final String? moduleId;
}

class EmbeddedInvokeResult {
const EmbeddedInvokeResult({
required this.ok,
this.value,
this.error,
});

final bool ok;
final Object? value;
final String? error;

factory EmbeddedInvokeResult.success(Object? value) =>
EmbeddedInvokeResult(ok: true, value: value);

factory EmbeddedInvokeResult.failure(String error) =>
EmbeddedInvokeResult(ok: false, error: error);
}

/// In-process Level-1 sandbox engine (WASM / QuickJS / declarative).
abstract class EmbeddedSandboxEngine {
SandboxEngine get engine;

/// Whether this build can actually execute guest code.
bool get isAvailable;

Future<EmbeddedInvokeResult> invoke(EmbeddedInvokeRequest request);

Future<void> dispose();
}

class EmbeddedEngineUnavailableException implements Exception {
EmbeddedEngineUnavailableException(this.message);
final String message;

@override
String toString() => 'EmbeddedEngineUnavailableException: $message';
}
Loading
Loading