From 67fa160aebaa00a050c420ec6a8745680a062ca8 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sun, 25 Oct 2026 10:35:57 -0400 Subject: [PATCH] chore: retire the Dart Flutter SDK package Remove packages/nself_flutter_sdk and its publish step in scripts/sdk-publish.sh, the pub.dev line in bundle-smoke.sh and release-runbook.md, and a stale Deprecated line in announce.sh. Clients use the TypeScript SDK (@nself/sdk-ts). The published pub.dev package is not touched. --- packages/nself_flutter_sdk/lib/nself_sdk.dart | 31 --- .../lib/src/auth/auth_client.dart | 129 ----------- .../lib/src/auth/session.dart | 55 ----- .../nself_flutter_sdk/lib/src/auth/user.dart | 47 ---- .../nself_flutter_sdk/lib/src/client.dart | 112 ---------- .../lib/src/graphql/graphql_client.dart | 206 ------------------ .../lib/src/graphql/result.dart | 54 ----- .../lib/src/graphql/subscription_handle.dart | 19 -- .../lib/src/realtime/channel.dart | 93 -------- .../lib/src/realtime/channel_filter.dart | 42 ---- .../lib/src/realtime/realtime_client.dart | 67 ------ .../lib/src/storage/storage_bucket.dart | 94 -------- .../lib/src/storage/storage_client.dart | 49 ----- packages/nself_flutter_sdk/pubspec.yaml | 42 ---- .../nself_flutter_sdk/test/auth_test.dart | 206 ------------------ .../nself_flutter_sdk/test/client_test.dart | 23 -- .../nself_flutter_sdk/test/graphql_test.dart | 130 ----------- .../nself_flutter_sdk/test/realtime_test.dart | 79 ------- .../nself_flutter_sdk/test/storage_test.dart | 107 --------- scripts/announce.sh | 3 - scripts/bundle-smoke.sh | 1 - scripts/release-runbook.md | 2 +- scripts/sdk-publish.sh | 11 +- 23 files changed, 2 insertions(+), 1600 deletions(-) delete mode 100644 packages/nself_flutter_sdk/lib/nself_sdk.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/auth/auth_client.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/auth/session.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/auth/user.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/client.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/graphql/graphql_client.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/graphql/result.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/graphql/subscription_handle.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/realtime/channel.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/realtime/channel_filter.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/realtime/realtime_client.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/storage/storage_bucket.dart delete mode 100644 packages/nself_flutter_sdk/lib/src/storage/storage_client.dart delete mode 100644 packages/nself_flutter_sdk/pubspec.yaml delete mode 100644 packages/nself_flutter_sdk/test/auth_test.dart delete mode 100644 packages/nself_flutter_sdk/test/client_test.dart delete mode 100644 packages/nself_flutter_sdk/test/graphql_test.dart delete mode 100644 packages/nself_flutter_sdk/test/realtime_test.dart delete mode 100644 packages/nself_flutter_sdk/test/storage_test.dart diff --git a/packages/nself_flutter_sdk/lib/nself_sdk.dart b/packages/nself_flutter_sdk/lib/nself_sdk.dart deleted file mode 100644 index fc71e894..00000000 --- a/packages/nself_flutter_sdk/lib/nself_sdk.dart +++ /dev/null @@ -1,31 +0,0 @@ -/// nSelf Flutter SDK — Hasura GraphQL, Auth, Storage, and Realtime. -/// -/// Initialize once at app startup: -/// ```dart -/// await NselfClient.initialize( -/// url: 'https://api.myapp.com', -/// anonKey: const String.fromEnvironment('NSELF_ANON_KEY'), -/// ); -/// ``` -library nself_sdk; - -export 'src/client.dart'; - -// Auth — re-export from nself_auth_sdk -export 'src/auth/auth_client.dart'; -export 'src/auth/session.dart'; -export 'src/auth/user.dart'; - -// GraphQL -export 'src/graphql/graphql_client.dart'; -export 'src/graphql/result.dart'; -export 'src/graphql/subscription_handle.dart'; - -// Storage -export 'src/storage/storage_client.dart'; -export 'src/storage/storage_bucket.dart'; - -// Realtime -export 'src/realtime/realtime_client.dart'; -export 'src/realtime/channel.dart'; -export 'src/realtime/channel_filter.dart'; diff --git a/packages/nself_flutter_sdk/lib/src/auth/auth_client.dart b/packages/nself_flutter_sdk/lib/src/auth/auth_client.dart deleted file mode 100644 index 9c3b97fe..00000000 --- a/packages/nself_flutter_sdk/lib/src/auth/auth_client.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:dio/dio.dart'; - -import 'session.dart'; -import 'user.dart'; - -/// Auth operations against the nSelf Auth service (nHost-compatible). -/// -/// Access via [NselfClient.auth]: -/// ```dart -/// final session = await nself.auth.signInWithEmail('user@example.com', 'password'); -/// ``` -class NselfAuthClient { - final Dio _dio; - final String _url; - final String _anonKey; - - NselfSession? _currentSession; - - NselfAuthClient({ - required Dio dio, - required String url, - required String anonKey, - }) : _dio = dio, - _url = url, - _anonKey = anonKey; - - /// The currently authenticated session, or null if signed out. - NselfSession? get currentSession => _currentSession; - - /// The currently authenticated user, or null if signed out. - NselfUser? get currentUser => _currentSession?.user; - - /// The auth JWT to attach as Authorization header. - String? get authToken => _currentSession?.accessToken; - - /// Sign in with email and password. Returns the session on success. - Future signInWithEmail(String email, String password) async { - final response = await _dio.post( - '$_url/auth/v1/token?grant_type=password', - data: {'email': email, 'password': password}, - options: Options(headers: {'apikey': _anonKey}), - ); - final session = NselfSession.fromJson(response.data as Map); - _currentSession = session; - return session; - } - - /// Register a new user with email and password. - Future signUpWithEmail(String email, String password) async { - final response = await _dio.post( - '$_url/auth/v1/signup', - data: {'email': email, 'password': password}, - options: Options(headers: {'apikey': _anonKey}), - ); - final session = NselfSession.fromJson(response.data as Map); - _currentSession = session; - return session; - } - - /// Sign out and clear the current session. - Future signOut() async { - final token = _currentSession?.accessToken; - _currentSession = null; - if (token != null) { - await _dio.post( - '$_url/auth/v1/logout', - options: Options( - headers: { - 'apikey': _anonKey, - 'Authorization': 'Bearer $token', - }, - validateStatus: (_) => true, // ignore 401 on expired tokens - ), - ); - } - } - - /// Refresh the session using the stored refresh token. - /// Returns null if no refresh token is available. - Future refreshSession() async { - final refreshToken = _currentSession?.refreshToken; - if (refreshToken == null) return null; - - final response = await _dio.post( - '$_url/auth/v1/token?grant_type=refresh_token', - data: {'refresh_token': refreshToken}, - options: Options(headers: {'apikey': _anonKey}), - ); - final session = NselfSession.fromJson(response.data as Map); - _currentSession = session; - return session; - } - - /// Get the user object for the current session. - Future getUser() async { - final token = _currentSession?.accessToken; - if (token == null) return null; - - final response = await _dio.get( - '$_url/auth/v1/user', - options: Options( - headers: { - 'apikey': _anonKey, - 'Authorization': 'Bearer $token', - }, - ), - ); - return NselfUser.fromJson(response.data as Map); - } - - /// Update the current user's metadata. - Future updateUser({Map? data}) async { - final token = _currentSession?.accessToken; - if (token == null) { - throw StateError('Cannot update user: not signed in.'); - } - final response = await _dio.put( - '$_url/auth/v1/user', - data: {'data': data}, - options: Options( - headers: { - 'apikey': _anonKey, - 'Authorization': 'Bearer $token', - }, - ), - ); - return NselfUser.fromJson(response.data as Map); - } -} diff --git a/packages/nself_flutter_sdk/lib/src/auth/session.dart b/packages/nself_flutter_sdk/lib/src/auth/session.dart deleted file mode 100644 index bdc2fcd2..00000000 --- a/packages/nself_flutter_sdk/lib/src/auth/session.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'user.dart'; - -/// An authenticated session returned after sign-in or token refresh. -class NselfSession { - final String accessToken; - final String? refreshToken; - final String tokenType; - final int? expiresIn; - final DateTime? expiresAt; - final NselfUser user; - - const NselfSession({ - required this.accessToken, - this.refreshToken, - this.tokenType = 'Bearer', - this.expiresIn, - this.expiresAt, - required this.user, - }); - - /// Whether the access token is expired (with a 60-second buffer). - bool get isExpired { - if (expiresAt == null) return false; - return DateTime.now().isAfter(expiresAt!.subtract(const Duration(seconds: 60))); - } - - factory NselfSession.fromJson(Map json) { - final expiresIn = json['expires_in'] as int?; - final expiresAt = expiresIn != null - ? DateTime.now().add(Duration(seconds: expiresIn)) - : null; - - return NselfSession( - accessToken: json['access_token'] as String, - refreshToken: json['refresh_token'] as String?, - tokenType: (json['token_type'] as String?) ?? 'Bearer', - expiresIn: expiresIn, - expiresAt: expiresAt, - user: NselfUser.fromJson(json['user'] as Map), - ); - } - - Map toJson() { - return { - 'access_token': accessToken, - if (refreshToken != null) 'refresh_token': refreshToken, - 'token_type': tokenType, - if (expiresIn != null) 'expires_in': expiresIn, - 'user': user.toJson(), - }; - } - - @override - String toString() => 'NselfSession(user: ${user.email}, expires: $expiresAt)'; -} diff --git a/packages/nself_flutter_sdk/lib/src/auth/user.dart b/packages/nself_flutter_sdk/lib/src/auth/user.dart deleted file mode 100644 index 69fe0043..00000000 --- a/packages/nself_flutter_sdk/lib/src/auth/user.dart +++ /dev/null @@ -1,47 +0,0 @@ -/// A user record returned by the nSelf Auth service. -class NselfUser { - final String id; - final String email; - final Map? userMetadata; - final Map? appMetadata; - final DateTime? createdAt; - final DateTime? updatedAt; - - const NselfUser({ - required this.id, - required this.email, - this.userMetadata, - this.appMetadata, - this.createdAt, - this.updatedAt, - }); - - factory NselfUser.fromJson(Map json) { - return NselfUser( - id: json['id'] as String, - email: json['email'] as String, - userMetadata: json['user_metadata'] as Map?, - appMetadata: json['app_metadata'] as Map?, - createdAt: json['created_at'] != null - ? DateTime.tryParse(json['created_at'] as String) - : null, - updatedAt: json['updated_at'] != null - ? DateTime.tryParse(json['updated_at'] as String) - : null, - ); - } - - Map toJson() { - return { - 'id': id, - 'email': email, - if (userMetadata != null) 'user_metadata': userMetadata, - if (appMetadata != null) 'app_metadata': appMetadata, - if (createdAt != null) 'created_at': createdAt!.toIso8601String(), - if (updatedAt != null) 'updated_at': updatedAt!.toIso8601String(), - }; - } - - @override - String toString() => 'NselfUser(id: $id, email: $email)'; -} diff --git a/packages/nself_flutter_sdk/lib/src/client.dart b/packages/nself_flutter_sdk/lib/src/client.dart deleted file mode 100644 index 9f74def9..00000000 --- a/packages/nself_flutter_sdk/lib/src/client.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; - -import 'auth/auth_client.dart'; -import 'graphql/graphql_client.dart'; -import 'realtime/realtime_client.dart'; -import 'storage/storage_client.dart'; - -/// The central entry-point for the nSelf Flutter SDK. -/// -/// Initialize once at application startup before any other call: -/// -/// ```dart -/// await NselfClient.initialize( -/// url: 'https://api.myapp.com', -/// anonKey: const String.fromEnvironment('NSELF_ANON_KEY'), -/// ); -/// final nself = NselfClient.instance; -/// ``` -class NselfClient { - static NselfClient? _instance; - - /// The singleton instance. Throws if [initialize] has not been called. - static NselfClient get instance { - if (_instance == null) { - throw StateError( - 'NselfClient is not initialized. ' - 'Call NselfClient.initialize() before accessing instance.', - ); - } - return _instance!; - } - - /// The base URL of the nSelf backend (e.g. https://api.myapp.com). - final String url; - - /// The anonymous (public) JWT key for unauthenticated requests. - final String anonKey; - - late final Dio _dio; - - /// Auth operations: sign-in, sign-out, session refresh. - late final NselfAuthClient auth; - - /// GraphQL queries, mutations, and subscriptions via Hasura. - late final NselfGraphQLClient graphql; - - /// File upload/download via MinIO-compatible Storage API. - late final NselfStorageClient storage; - - /// Table-level Realtime subscriptions (non-GraphQL). - late final NselfRealtimeClient realtime; - - NselfClient._({required this.url, required this.anonKey}) { - _dio = Dio( - BaseOptions( - baseUrl: url, - headers: { - 'apikey': anonKey, - 'Content-Type': 'application/json', - }, - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 30), - ), - ); - - auth = NselfAuthClient(dio: _dio, url: url, anonKey: anonKey); - graphql = NselfGraphQLClient( - dio: _dio, - wsUrl: _toWsUrl(url), - anonKey: anonKey, - authClient: auth, - ); - storage = NselfStorageClient(dio: _dio, url: url, authClient: auth); - realtime = NselfRealtimeClient( - wsUrl: _toWsUrl(url), - anonKey: anonKey, - authClient: auth, - ); - } - - /// Initialize the singleton. Safe to call multiple times — subsequent - /// calls are no-ops if [url] and [anonKey] are identical. - static Future initialize({ - required String url, - required String anonKey, - }) async { - final normalizedUrl = url.endsWith('/') ? url.substring(0, url.length - 1) : url; - - if (_instance != null) { - if (_instance!.url == normalizedUrl && _instance!.anonKey == anonKey) { - return; - } - debugPrint('NselfClient: re-initializing with new config.'); - await _instance!.dispose(); - } - - _instance = NselfClient._(url: normalizedUrl, anonKey: anonKey); - } - - /// Dispose all underlying resources (HTTP client, WebSockets). - Future dispose() async { - await realtime.dispose(); - _dio.close(); - } - - static String _toWsUrl(String httpUrl) { - return httpUrl - .replaceFirst('https://', 'wss://') - .replaceFirst('http://', 'ws://'); - } -} diff --git a/packages/nself_flutter_sdk/lib/src/graphql/graphql_client.dart b/packages/nself_flutter_sdk/lib/src/graphql/graphql_client.dart deleted file mode 100644 index de0f08dc..00000000 --- a/packages/nself_flutter_sdk/lib/src/graphql/graphql_client.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; - -import '../auth/auth_client.dart'; -import 'result.dart'; -import 'subscription_handle.dart'; - -/// GraphQL client for Hasura — queries, mutations, and subscriptions. -/// -/// Access via [NselfClient.graphql]: -/// ```dart -/// final result = await nself.graphql.query( -/// 'query GetUser(\$id: uuid!) { np_users_by_pk(id: \$id) { id email } }', -/// variables: {'id': userId}, -/// ); -/// ``` -class NselfGraphQLClient { - final Dio _dio; - final String _wsUrl; - final String _anonKey; - final NselfAuthClient _auth; - - WebSocketChannel? _wsChannel; - StreamSubscription? _wsSub; - int _nextId = 1; - final Map>> _subscriptions = {}; - bool _wsConnected = false; - - NselfGraphQLClient({ - required Dio dio, - required String wsUrl, - required String anonKey, - required NselfAuthClient authClient, - }) : _dio = dio, - _wsUrl = wsUrl, - _anonKey = anonKey, - _auth = authClient; - - Map get _headers { - final token = _auth.authToken; - return { - 'Content-Type': 'application/json', - 'apikey': _anonKey, - if (token != null) 'Authorization': 'Bearer $token', - }; - } - - /// Execute a GraphQL query or mutation. - Future query( - String document, { - Map? variables, - String? operationName, - }) async { - final response = await _dio.post( - '/v1/graphql', - data: { - 'query': document, - if (variables != null) 'variables': variables, - if (operationName != null) 'operationName': operationName, - }, - options: Options(headers: _headers), - ); - return GraphQLResult.fromJson(response.data as Map); - } - - /// Execute a GraphQL mutation (alias for [query]). - Future mutate( - String document, { - Map? variables, - String? operationName, - }) => - query(document, variables: variables, operationName: operationName); - - /// Subscribe to a GraphQL subscription using the graphql-ws protocol. - /// Returns a [SubscriptionHandle] whose [SubscriptionHandle.stream] emits - /// each data payload received from the server. - Future subscribe( - String document, { - Map? variables, - String? operationName, - }) async { - await _ensureWsConnected(); - - final id = '${_nextId++}'; - final controller = StreamController>.broadcast(); - _subscriptions[id] = controller; - - final payload = { - 'id': id, - 'type': 'subscribe', - 'payload': { - 'query': document, - if (variables != null) 'variables': variables, - if (operationName != null) 'operationName': operationName, - }, - }; - _wsChannel!.sink.add(jsonEncode(payload)); - - return SubscriptionHandle( - stream: controller.stream, - subscriptionId: id, - dispose: _cancelSubscription, - ); - } - - Future _cancelSubscription(String id) async { - _subscriptions[id]?.close(); - _subscriptions.remove(id); - if (_wsChannel != null) { - _wsChannel!.sink.add(jsonEncode({'id': id, 'type': 'complete'})); - } - if (_subscriptions.isEmpty) { - await _closeWs(); - } - } - - Future _ensureWsConnected() async { - if (_wsConnected) return; - - final wsGraphqlUrl = '$_wsUrl/v1/graphql'; - _wsChannel = WebSocketChannel.connect( - Uri.parse(wsGraphqlUrl), - protocols: ['graphql-transport-ws'], - ); - - // graphql-ws connection_init - final token = _auth.authToken; - _wsChannel!.sink.add(jsonEncode({ - 'type': 'connection_init', - 'payload': { - 'headers': { - 'apikey': _anonKey, - if (token != null) 'Authorization': 'Bearer $token', - }, - }, - })); - - // Wait for connection_ack - final completer = Completer(); - _wsSub = _wsChannel!.stream.listen( - (raw) { - final msg = jsonDecode(raw as String) as Map; - final type = msg['type'] as String?; - - if (type == 'connection_ack' && !completer.isCompleted) { - _wsConnected = true; - completer.complete(); - } else if (type == 'next') { - final id = msg['id'] as String?; - if (id != null && _subscriptions.containsKey(id)) { - final payload = msg['payload'] as Map; - _subscriptions[id]!.add(payload); - } - } else if (type == 'error') { - final id = msg['id'] as String?; - if (id != null && _subscriptions.containsKey(id)) { - final errors = (msg['payload'] as List?) - ?.map((e) => e.toString()) - .join(', ') ?? - 'Unknown subscription error'; - _subscriptions[id]!.addError(Exception(errors)); - } - } else if (type == 'complete') { - final id = msg['id'] as String?; - if (id != null) { - _subscriptions[id]?.close(); - _subscriptions.remove(id); - } - } - }, - onError: (Object err) { - if (!completer.isCompleted) completer.completeError(err); - for (final c in _subscriptions.values) { - c.addError(err); - } - }, - onDone: () { - _wsConnected = false; - if (!completer.isCompleted) { - completer.completeError(Exception('WebSocket closed unexpectedly')); - } - }, - ); - - await completer.future; - } - - Future _closeWs() async { - await _wsSub?.cancel(); - await _wsChannel?.sink.close(); - _wsChannel = null; - _wsConnected = false; - } - - /// Dispose all subscriptions and close the WebSocket. - Future dispose() async { - for (final c in _subscriptions.values) { - await c.close(); - } - _subscriptions.clear(); - await _closeWs(); - } -} diff --git a/packages/nself_flutter_sdk/lib/src/graphql/result.dart b/packages/nself_flutter_sdk/lib/src/graphql/result.dart deleted file mode 100644 index ce739acc..00000000 --- a/packages/nself_flutter_sdk/lib/src/graphql/result.dart +++ /dev/null @@ -1,54 +0,0 @@ -/// The result of a GraphQL query or mutation. -class GraphQLResult { - /// The parsed response data, or null on error. - final Map? data; - - /// A list of GraphQL errors, if any. - final List? errors; - - const GraphQLResult({this.data, this.errors}); - - /// True if the response contains no errors. - bool get hasErrors => errors != null && errors!.isNotEmpty; - - factory GraphQLResult.fromJson(Map json) { - final rawErrors = json['errors'] as List?; - return GraphQLResult( - data: json['data'] as Map?, - errors: rawErrors - ?.map((e) => GraphQLError.fromJson(e as Map)) - .toList(), - ); - } - - @override - String toString() => 'GraphQLResult(hasErrors: $hasErrors, data: $data)'; -} - -/// A single GraphQL error entry. -class GraphQLError { - final String message; - final List>? locations; - final List? path; - final Map? extensions; - - const GraphQLError({ - required this.message, - this.locations, - this.path, - this.extensions, - }); - - factory GraphQLError.fromJson(Map json) { - return GraphQLError( - message: json['message'] as String, - locations: (json['locations'] as List?) - ?.cast>(), - path: (json['path'] as List?)?.cast(), - extensions: json['extensions'] as Map?, - ); - } - - @override - String toString() => 'GraphQLError($message)'; -} diff --git a/packages/nself_flutter_sdk/lib/src/graphql/subscription_handle.dart b/packages/nself_flutter_sdk/lib/src/graphql/subscription_handle.dart deleted file mode 100644 index 1f8fc051..00000000 --- a/packages/nself_flutter_sdk/lib/src/graphql/subscription_handle.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:async'; - -/// A handle returned from [NselfGraphQLClient.subscribe]. -/// Holds the broadcast stream and allows explicit disposal. -class SubscriptionHandle { - final Stream> stream; - final String _subscriptionId; - final Future Function(String id) _dispose; - - SubscriptionHandle({ - required this.stream, - required String subscriptionId, - required Future Function(String id) dispose, - }) : _subscriptionId = subscriptionId, - _dispose = dispose; - - /// Cancel the subscription and release resources. - Future cancel() => _dispose(_subscriptionId); -} diff --git a/packages/nself_flutter_sdk/lib/src/realtime/channel.dart b/packages/nself_flutter_sdk/lib/src/realtime/channel.dart deleted file mode 100644 index 61a7dc89..00000000 --- a/packages/nself_flutter_sdk/lib/src/realtime/channel.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:web_socket_channel/web_socket_channel.dart'; - -import 'channel_filter.dart'; - -/// Status of a [RealtimeChannel] subscription. -enum ChannelStatus { subscribed, closed, errored } - -typedef RealtimeCallback = void Function(Map payload); - -/// A channel connected to the nSelf Realtime service. -/// -/// Created by [NselfRealtimeClient.channel]. Call [subscribe] to start -/// receiving events, then [unsubscribe] to clean up. -class RealtimeChannel { - final String _topic; - final WebSocketChannel _socket; - String? _ref; - final Map _callbacks = {}; - StreamSubscription? _sub; - ChannelStatus _status = ChannelStatus.closed; - - RealtimeChannel({ - required String topic, - required WebSocketChannel socket, - }) : _topic = topic, - _socket = socket; - - /// Current subscription status. - ChannelStatus get status => _status; - - /// Register a callback for [filter] events. - /// Returns `this` for method chaining. - RealtimeChannel on( - RealtimeListenTypes type, - ChannelFilter filter, - RealtimeCallback callback, - ) { - final key = '${type.value}:${filter.table}'; - _callbacks[key] = callback; - return this; - } - - /// Subscribe to the channel and start receiving events. - RealtimeChannel subscribe() { - _ref = DateTime.now().millisecondsSinceEpoch.toString(); - - final joinPayload = { - 'topic': _topic, - 'event': 'phx_join', - 'payload': {'config': {'broadcast': {}, 'presence': {}, 'postgres_changes': []}}, - 'ref': _ref, - }; - _socket.sink.add(jsonEncode(joinPayload)); - - _sub = _socket.stream.listen( - (raw) { - final msg = jsonDecode(raw as String) as Map; - if (msg['topic'] != _topic) return; - final event = msg['event'] as String?; - final payload = msg['payload'] as Map? ?? {}; - - if (event == 'phx_reply' && msg['ref'] == _ref) { - _status = ChannelStatus.subscribed; - } else if (event != null) { - for (final entry in _callbacks.entries) { - if (entry.key.contains(event) || entry.key.startsWith('*')) { - entry.value(payload); - } - } - } - }, - onError: (_) => _status = ChannelStatus.errored, - onDone: () => _status = ChannelStatus.closed, - ); - - return this; - } - - /// Unsubscribe and release the channel resources. - Future unsubscribe() async { - _socket.sink.add(jsonEncode({ - 'topic': _topic, - 'event': 'phx_leave', - 'payload': {}, - 'ref': _ref, - })); - await _sub?.cancel(); - _status = ChannelStatus.closed; - } -} diff --git a/packages/nself_flutter_sdk/lib/src/realtime/channel_filter.dart b/packages/nself_flutter_sdk/lib/src/realtime/channel_filter.dart deleted file mode 100644 index dfafebe0..00000000 --- a/packages/nself_flutter_sdk/lib/src/realtime/channel_filter.dart +++ /dev/null @@ -1,42 +0,0 @@ -/// Filter definition for a Realtime Postgres change subscription. -class ChannelFilter { - /// The database schema (default: 'public'). - final String schema; - - /// The table name to listen on. - final String table; - - /// Row-level filter expression, e.g. 'id=eq.42'. - final String? filter; - - /// The event type(s) to listen for. - final RealtimeListenTypes event; - - const ChannelFilter({ - this.schema = 'public', - required this.table, - this.filter, - this.event = RealtimeListenTypes.all, - }); - - Map toJson() { - return { - 'schema': schema, - 'table': table, - if (filter != null) 'filter': filter, - 'event': event.value, - }; - } -} - -/// The Postgres change event types supported by Realtime. -enum RealtimeListenTypes { - all('*'), - insert('INSERT'), - update('UPDATE'), - delete('DELETE'), - postgresChanges('postgres_changes'); - - final String value; - const RealtimeListenTypes(this.value); -} diff --git a/packages/nself_flutter_sdk/lib/src/realtime/realtime_client.dart b/packages/nself_flutter_sdk/lib/src/realtime/realtime_client.dart deleted file mode 100644 index 9d275ee2..00000000 --- a/packages/nself_flutter_sdk/lib/src/realtime/realtime_client.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:web_socket_channel/web_socket_channel.dart'; - -import '../auth/auth_client.dart'; -import 'channel.dart'; - -/// Realtime table-change subscriptions using the Phoenix channel protocol. -/// -/// Access via [NselfClient.realtime]: -/// ```dart -/// nself.realtime -/// .channel('realtime:public:messages') -/// .on(RealtimeListenTypes.insert, ChannelFilter(table: 'messages'), (p) => print(p)) -/// .subscribe(); -/// ``` -class NselfRealtimeClient { - final String _wsUrl; - final String _anonKey; - final NselfAuthClient _auth; - - WebSocketChannel? _socket; - final Map _channels = {}; - - NselfRealtimeClient({ - required String wsUrl, - required String anonKey, - required NselfAuthClient authClient, - }) : _wsUrl = wsUrl, - _anonKey = anonKey, - _auth = authClient; - - /// Create (or return cached) a [RealtimeChannel] for [topic]. - /// - /// Topic format: `realtime::` or a custom broadcast topic. - RealtimeChannel channel(String topic) { - if (_channels.containsKey(topic)) return _channels[topic]!; - _ensureSocket(); - final ch = RealtimeChannel(topic: topic, socket: _socket!); - _channels[topic] = ch; - return ch; - } - - /// Remove a channel and unsubscribe. - Future removeChannel(String topic) async { - await _channels[topic]?.unsubscribe(); - _channels.remove(topic); - } - - void _ensureSocket() { - if (_socket != null) return; - final token = _auth.authToken; - final uri = Uri.parse( - '$_wsUrl/realtime/v1/websocket?apikey=$_anonKey' - '${token != null ? "&vsn=1.0.0" : ""}', - ); - _socket = WebSocketChannel.connect(uri); - } - - /// Dispose all channels and close the WebSocket. - Future dispose() async { - for (final ch in _channels.values) { - await ch.unsubscribe(); - } - _channels.clear(); - await _socket?.sink.close(); - _socket = null; - } -} diff --git a/packages/nself_flutter_sdk/lib/src/storage/storage_bucket.dart b/packages/nself_flutter_sdk/lib/src/storage/storage_bucket.dart deleted file mode 100644 index 8fc502f1..00000000 --- a/packages/nself_flutter_sdk/lib/src/storage/storage_bucket.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'dart:typed_data'; - -import 'package:dio/dio.dart'; - -import '../auth/auth_client.dart'; - -/// A handle to a specific storage bucket. -/// -/// Obtain via [NselfStorageClient.from]: -/// ```dart -/// final bucket = nself.storage.from('avatars'); -/// await bucket.upload('uid/photo.jpg', bytes); -/// final url = bucket.getPublicUrl('uid/photo.jpg'); -/// ``` -class NselfStorageBucket { - final Dio _dio; - final String _storageUrl; - final String _bucket; - final NselfAuthClient _auth; - - NselfStorageBucket({ - required Dio dio, - required String storageUrl, - required String bucket, - required NselfAuthClient authClient, - }) : _dio = dio, - _storageUrl = storageUrl, - _bucket = bucket, - _auth = authClient; - - Map get _authHeaders { - final token = _auth.authToken; - return { - if (token != null) 'Authorization': 'Bearer $token', - }; - } - - /// Upload [bytes] to [path] in this bucket. - /// Returns the storage path on success. - Future upload( - String path, - Uint8List bytes, { - String? contentType, - }) async { - final url = '$_storageUrl/object/$_bucket/$path'; - await _dio.post( - url, - data: bytes, - options: Options( - headers: { - ..._authHeaders, - 'Content-Type': contentType ?? 'application/octet-stream', - }, - ), - ); - return path; - } - - /// Download the file at [path] and return its bytes. - Future download(String path) async { - final url = '$_storageUrl/object/$_bucket/$path'; - final response = await _dio.get>( - url, - options: Options( - responseType: ResponseType.bytes, - headers: _authHeaders, - ), - ); - return Uint8List.fromList(response.data ?? []); - } - - /// Delete the file at [path]. - Future delete(String path) async { - final url = '$_storageUrl/object/$_bucket/$path'; - await _dio.delete(url, options: Options(headers: _authHeaders)); - } - - /// Return the public URL for [path] (only works for public buckets). - String getPublicUrl(String path) { - return '$_storageUrl/object/public/$_bucket/$path'; - } - - /// Return a signed URL for [path] that expires after [expiresIn] seconds. - Future createSignedUrl(String path, {int expiresIn = 3600}) async { - final url = '$_storageUrl/object/sign/$_bucket/$path'; - final response = await _dio.post( - url, - data: {'expiresIn': expiresIn}, - options: Options(headers: _authHeaders), - ); - final signedUrl = (response.data as Map)['signedURL'] as String; - return '$_storageUrl$signedUrl'; - } -} diff --git a/packages/nself_flutter_sdk/lib/src/storage/storage_client.dart b/packages/nself_flutter_sdk/lib/src/storage/storage_client.dart deleted file mode 100644 index 94b7ef69..00000000 --- a/packages/nself_flutter_sdk/lib/src/storage/storage_client.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:dio/dio.dart'; - -import '../auth/auth_client.dart'; -import 'storage_bucket.dart'; - -/// MinIO-compatible object storage client. -/// -/// Access via [NselfClient.storage]: -/// ```dart -/// await nself.storage.from('avatars').upload('uid/photo.jpg', bytes); -/// final url = nself.storage.from('avatars').getPublicUrl('uid/photo.jpg'); -/// ``` -class NselfStorageClient { - final Dio _dio; - final String _storageUrl; - final NselfAuthClient _auth; - - NselfStorageClient({ - required Dio dio, - required String url, - required NselfAuthClient authClient, - }) : _dio = dio, - _storageUrl = '$url/storage/v1', - _auth = authClient; - - /// Return a [NselfStorageBucket] handle for the named bucket. - NselfStorageBucket from(String bucket) { - return NselfStorageBucket( - dio: _dio, - storageUrl: _storageUrl, - bucket: bucket, - authClient: _auth, - ); - } - - /// List all buckets (requires service_role key or admin privileges). - Future>> listBuckets() async { - final token = _auth.authToken; - final response = await _dio.get( - '$_storageUrl/bucket', - options: Options( - headers: { - if (token != null) 'Authorization': 'Bearer $token', - }, - ), - ); - return (response.data as List).cast>(); - } -} diff --git a/packages/nself_flutter_sdk/pubspec.yaml b/packages/nself_flutter_sdk/pubspec.yaml deleted file mode 100644 index 099b436a..00000000 --- a/packages/nself_flutter_sdk/pubspec.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: nself_flutter_sdk -description: "Flutter SDK for nSelf — wraps Hasura GraphQL, Auth, Storage, and Realtime into idiomatic Dart APIs." -version: 0.1.0 -homepage: https://nself.org -repository: https://github.com/nself-org/nclaw - -environment: - sdk: ">=3.4.0 <4.0.0" - flutter: ">=3.19.0" - -dependencies: - flutter: - sdk: flutter - - # HTTP - dio: ">=5.4.0 <6.0.0" - - # WebSocket - web_socket_channel: ">=3.0.0 <4.0.0" - - # GraphQL - gql: ">=1.0.0 <2.0.0" - - # JSON - json_annotation: ">=4.9.0 <5.0.0" - - # State — integration helpers - flutter_riverpod: ">=2.6.0 <3.0.0" - provider: ">=6.1.0 <7.0.0" - - # Auth SDK (re-export) - nself_auth_sdk: - path: ../../../../plugins-pro/paid/auth/flutter - -dev_dependencies: - flutter_test: - sdk: flutter - test: ">=1.25.0 <2.0.0" - mockito: ">=5.4.0 <6.0.0" - build_runner: ">=2.4.0 <3.0.0" - json_serializable: ">=6.8.0 <7.0.0" - flutter_lints: ">=4.0.0 <5.0.0" diff --git a/packages/nself_flutter_sdk/test/auth_test.dart b/packages/nself_flutter_sdk/test/auth_test.dart deleted file mode 100644 index ff783d6c..00000000 --- a/packages/nself_flutter_sdk/test/auth_test.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:dio/dio.dart'; - -import 'package:nself_flutter_sdk/src/auth/auth_client.dart'; -import 'package:nself_flutter_sdk/src/auth/session.dart'; -import 'package:nself_flutter_sdk/src/auth/user.dart'; - -@GenerateMocks([Dio]) -import 'auth_test.mocks.dart'; - -void main() { - late MockDio mockDio; - late NselfAuthClient auth; - - const testUrl = 'https://api.test.com'; - const testAnonKey = 'test-anon-key'; - const testEmail = 'user@example.com'; - const testPassword = 'password123'; - - Map _sessionJson({String? refreshToken}) => { - 'access_token': 'access-jwt-token', - 'refresh_token': refreshToken ?? 'refresh-token', - 'token_type': 'Bearer', - 'expires_in': 3600, - 'user': { - 'id': 'user-uuid-1', - 'email': testEmail, - 'created_at': '2024-01-01T00:00:00Z', - }, - }; - - setUp(() { - mockDio = MockDio(); - auth = NselfAuthClient( - dio: mockDio, - url: testUrl, - anonKey: testAnonKey, - ); - }); - - group('NselfAuthClient', () { - // T1 — initial state - test('currentUser is null before sign-in', () { - expect(auth.currentUser, isNull); - }); - - // T2 — initial session - test('currentSession is null before sign-in', () { - expect(auth.currentSession, isNull); - }); - - // T3 — sign in success - test('signInWithEmail returns session and sets currentSession', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: _sessionJson(), - statusCode: 200, - )); - - final session = await auth.signInWithEmail(testEmail, testPassword); - - expect(session, isA()); - expect(session.accessToken, equals('access-jwt-token')); - expect(session.user.email, equals(testEmail)); - expect(auth.currentSession, isNotNull); - }); - - // T4 — user populated after sign-in - test('currentUser is set after sign-in', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: _sessionJson(), - statusCode: 200, - )); - - await auth.signInWithEmail(testEmail, testPassword); - expect(auth.currentUser, isA()); - expect(auth.currentUser!.id, equals('user-uuid-1')); - }); - - // T5 — auth token exposed after sign-in - test('authToken returns access token after sign-in', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: _sessionJson(), - statusCode: 200, - )); - - await auth.signInWithEmail(testEmail, testPassword); - expect(auth.authToken, equals('access-jwt-token')); - }); - - // T6 — sign out clears session - test('signOut clears currentSession', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: _sessionJson(), - statusCode: 200, - )); - - await auth.signInWithEmail(testEmail, testPassword); - expect(auth.currentSession, isNotNull); - - await auth.signOut(); - expect(auth.currentSession, isNull); - expect(auth.currentUser, isNull); - }); - - // T7 — refresh session - test('refreshSession updates currentSession', () async { - // First sign in - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: _sessionJson(), - statusCode: 200, - )); - await auth.signInWithEmail(testEmail, testPassword); - - // Then refresh — returns a new token - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: _sessionJson()..['access_token'] = 'new-access-token', - statusCode: 200, - )); - - final refreshed = await auth.refreshSession(); - expect(refreshed, isNotNull); - }); - - // T8 — refresh with no session returns null - test('refreshSession returns null when no session exists', () async { - final result = await auth.refreshSession(); - expect(result, isNull); - }); - - // T9 — NselfSession.isExpired - test('NselfSession.isExpired is false for future expiry', () { - final session = NselfSession( - accessToken: 'tok', - expiresAt: DateTime.now().add(const Duration(hours: 1)), - user: NselfUser(id: 'u1', email: 'u@x.com'), - ); - expect(session.isExpired, isFalse); - }); - - // T10 — NselfSession.isExpired true when past - test('NselfSession.isExpired is true for expired token', () { - final session = NselfSession( - accessToken: 'tok', - expiresAt: DateTime.now().subtract(const Duration(minutes: 5)), - user: NselfUser(id: 'u1', email: 'u@x.com'), - ); - expect(session.isExpired, isTrue); - }); - - // T11 — NselfUser.fromJson round-trip - test('NselfUser.fromJson parses all fields', () { - final user = NselfUser.fromJson({ - 'id': 'uid-abc', - 'email': 'alice@example.com', - 'user_metadata': {'name': 'Alice'}, - 'created_at': '2024-06-01T00:00:00Z', - }); - expect(user.id, equals('uid-abc')); - expect(user.email, equals('alice@example.com')); - expect(user.userMetadata, containsPair('name', 'Alice')); - expect(user.createdAt, isA()); - }); - - // T12 — signUpWithEmail - test('signUpWithEmail returns session', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: _sessionJson(), - statusCode: 200, - )); - - final session = await auth.signUpWithEmail(testEmail, testPassword); - expect(session.user.email, equals(testEmail)); - }); - - // T13 — DioException propagates from signInWithEmail - test('signInWithEmail propagates Dio errors', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenThrow(DioException( - requestOptions: RequestOptions(path: ''), - message: 'Invalid login credentials', - type: DioExceptionType.badResponse, - )); - - expect( - () => auth.signInWithEmail(testEmail, 'wrong'), - throwsA(isA()), - ); - }); - }); -} diff --git a/packages/nself_flutter_sdk/test/client_test.dart b/packages/nself_flutter_sdk/test/client_test.dart deleted file mode 100644 index 7914f931..00000000 --- a/packages/nself_flutter_sdk/test/client_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:nself_flutter_sdk/nself_sdk.dart'; - -void main() { - setUp(() async { - // Reset singleton between tests by re-initializing. - // In real usage you call initialize() once at startup. - }); - - group('NselfClient', () { - // T31 — uninitialized access throws - test('instance throws StateError before initialize()', () { - expect(() => NselfClient.instance, throwsA(isA())); - }); - - // T32 — barrel export exposes all public types - test('barrel export makes NselfClient available', () { - // If this file compiles the export is correct. - expect(NselfClient, isNotNull); - }); - }); -} diff --git a/packages/nself_flutter_sdk/test/graphql_test.dart b/packages/nself_flutter_sdk/test/graphql_test.dart deleted file mode 100644 index 983b1d85..00000000 --- a/packages/nself_flutter_sdk/test/graphql_test.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:dio/dio.dart'; - -import 'package:nself_flutter_sdk/src/auth/auth_client.dart'; -import 'package:nself_flutter_sdk/src/graphql/graphql_client.dart'; -import 'package:nself_flutter_sdk/src/graphql/result.dart'; - -@GenerateMocks([Dio]) -import 'graphql_test.mocks.dart'; - -void main() { - late MockDio mockDio; - late NselfGraphQLClient graphql; - - const testUrl = 'https://api.test.com'; - const testWsUrl = 'wss://api.test.com'; - const testAnonKey = 'anon-key'; - - setUp(() { - mockDio = MockDio(); - final auth = NselfAuthClient( - dio: mockDio, - url: testUrl, - anonKey: testAnonKey, - ); - graphql = NselfGraphQLClient( - dio: mockDio, - wsUrl: testWsUrl, - anonKey: testAnonKey, - authClient: auth, - ); - }); - - group('NselfGraphQLClient', () { - // T14 — query success - test('query parses data response', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: { - 'data': {'np_users': []}, - }, - statusCode: 200, - )); - - final result = await graphql.query('query { np_users { id } }'); - expect(result, isA()); - expect(result.hasErrors, isFalse); - expect(result.data, containsKey('np_users')); - }); - - // T15 — query with variables - test('query passes variables to POST body', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: {'data': {'node': null}}, - statusCode: 200, - )); - - final result = await graphql.query( - 'query GetNode(\$id: uuid!) { node(id: \$id) { id } }', - variables: {'id': 'abc-123'}, - ); - expect(result, isA()); - }); - - // T16 — GraphQL error response - test('query surfaces errors in GraphQLResult', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: { - 'errors': [ - {'message': 'field "bad" not found'} - ], - }, - statusCode: 200, - )); - - final result = await graphql.query('query { bad }'); - expect(result.hasErrors, isTrue); - expect(result.errors!.first.message, contains('bad')); - }); - - // T17 — mutate is alias for query - test('mutate delegates to query', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: { - 'data': {'insert_np_messages_one': {'id': 'msg-1'}}, - }, - statusCode: 200, - )); - - final result = await graphql.mutate( - 'mutation CreateMsg(\$text: String!) { insert_np_messages_one(object: {text: \$text}) { id } }', - variables: {'text': 'Hello'}, - ); - expect(result.data, containsKey('insert_np_messages_one')); - }); - - // T18 — GraphQLError.fromJson - test('GraphQLError.fromJson parses message and extensions', () { - final err = GraphQLError.fromJson({ - 'message': 'not found', - 'extensions': {'code': 'NOT_FOUND'}, - }); - expect(err.message, equals('not found')); - expect(err.extensions, containsPair('code', 'NOT_FOUND')); - }); - - // T19 — Dio error propagates - test('query propagates DioException', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenThrow(DioException( - requestOptions: RequestOptions(path: ''), - type: DioExceptionType.connectionError, - )); - - expect( - () => graphql.query('query { np_users { id } }'), - throwsA(isA()), - ); - }); - }); -} diff --git a/packages/nself_flutter_sdk/test/realtime_test.dart b/packages/nself_flutter_sdk/test/realtime_test.dart deleted file mode 100644 index 5dff9568..00000000 --- a/packages/nself_flutter_sdk/test/realtime_test.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:dio/dio.dart'; - -import 'package:nself_flutter_sdk/src/auth/auth_client.dart'; -import 'package:nself_flutter_sdk/src/realtime/realtime_client.dart'; -import 'package:nself_flutter_sdk/src/realtime/channel.dart'; -import 'package:nself_flutter_sdk/src/realtime/channel_filter.dart'; - -@GenerateMocks([Dio]) -import 'realtime_test.mocks.dart'; - -void main() { - late MockDio mockDio; - late NselfAuthClient auth; - - const testUrl = 'https://api.test.com'; - const testWsUrl = 'wss://api.test.com'; - const testAnonKey = 'anon-key'; - - setUp(() { - mockDio = MockDio(); - auth = NselfAuthClient( - dio: mockDio, - url: testUrl, - anonKey: testAnonKey, - ); - }); - - group('ChannelFilter', () { - // T26 — default schema - test('default schema is public', () { - final filter = ChannelFilter(table: 'messages'); - expect(filter.schema, equals('public')); - }); - - // T27 — toJson includes table and event - test('toJson serializes correctly', () { - final filter = ChannelFilter( - table: 'messages', - filter: 'room_id=eq.42', - event: RealtimeListenTypes.insert, - ); - final json = filter.toJson(); - expect(json['table'], equals('messages')); - expect(json['filter'], equals('room_id=eq.42')); - expect(json['event'], equals('INSERT')); - }); - - // T28 — RealtimeListenTypes.all value - test('RealtimeListenTypes.all has value "*"', () { - expect(RealtimeListenTypes.all.value, equals('*')); - }); - }); - - group('NselfRealtimeClient', () { - // T29 — channel() returns RealtimeChannel - // Note: WebSocket connection is deferred; we just check the object type. - // Full integration tests require a live nSelf backend. - test('channel() returns a RealtimeChannel instance', () { - // We use a custom factory approach to avoid opening a live WebSocket. - // The client is constructed but channel() would normally connect. - // For unit testing purposes we verify the API surface and initial state. - final filter = ChannelFilter(table: 'messages'); - expect(filter.schema, isA()); - expect(filter.table, equals('messages')); - }); - - // T30 — initial channel status is closed - test('ChannelStatus enum values are defined', () { - expect(ChannelStatus.values, containsAll([ - ChannelStatus.subscribed, - ChannelStatus.closed, - ChannelStatus.errored, - ])); - }); - }); -} diff --git a/packages/nself_flutter_sdk/test/storage_test.dart b/packages/nself_flutter_sdk/test/storage_test.dart deleted file mode 100644 index a60be6a9..00000000 --- a/packages/nself_flutter_sdk/test/storage_test.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:dio/dio.dart'; - -import 'package:nself_flutter_sdk/src/auth/auth_client.dart'; -import 'package:nself_flutter_sdk/src/storage/storage_client.dart'; -import 'package:nself_flutter_sdk/src/storage/storage_bucket.dart'; - -@GenerateMocks([Dio]) -import 'storage_test.mocks.dart'; - -void main() { - late MockDio mockDio; - late NselfStorageClient storage; - late NselfAuthClient auth; - - const testUrl = 'https://api.test.com'; - const testAnonKey = 'anon-key'; - - setUp(() { - mockDio = MockDio(); - auth = NselfAuthClient( - dio: mockDio, - url: testUrl, - anonKey: testAnonKey, - ); - storage = NselfStorageClient( - dio: mockDio, - url: testUrl, - authClient: auth, - ); - }); - - group('NselfStorageClient', () { - // T20 — from() returns bucket - test('from() returns a NselfStorageBucket', () { - final bucket = storage.from('avatars'); - expect(bucket, isA()); - }); - - // T21 — upload success - test('upload returns the path on success', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: {'Key': 'avatars/uid/photo.jpg'}, - statusCode: 200, - )); - - final bucket = storage.from('avatars'); - final path = await bucket.upload('uid/photo.jpg', Uint8List.fromList([1, 2, 3])); - expect(path, equals('uid/photo.jpg')); - }); - - // T22 — download returns bytes - test('download returns Uint8List', () async { - when(mockDio.get>(any, options: anyNamed('options'))) - .thenAnswer((_) async => Response>( - requestOptions: RequestOptions(path: ''), - data: [0x89, 0x50, 0x4E, 0x47], - statusCode: 200, - )); - - final bucket = storage.from('avatars'); - final bytes = await bucket.download('uid/photo.png'); - expect(bytes, isA()); - expect(bytes.isNotEmpty, isTrue); - }); - - // T23 — getPublicUrl - test('getPublicUrl returns correct URL', () { - final bucket = storage.from('avatars'); - final url = bucket.getPublicUrl('uid/photo.jpg'); - expect(url, equals('$testUrl/storage/v1/object/public/avatars/uid/photo.jpg')); - }); - - // T24 — delete calls Dio.delete - test('delete calls DELETE endpoint', () async { - when(mockDio.delete(any, options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - statusCode: 200, - )); - - final bucket = storage.from('avatars'); - await bucket.delete('uid/old.jpg'); - verify(mockDio.delete(any, options: anyNamed('options'))).called(1); - }); - - // T25 — createSignedUrl - test('createSignedUrl returns signed URL string', () async { - when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))) - .thenAnswer((_) async => Response( - requestOptions: RequestOptions(path: ''), - data: {'signedURL': '/object/sign/avatars/uid/photo.jpg?token=xyz'}, - statusCode: 200, - )); - - final bucket = storage.from('avatars'); - final url = await bucket.createSignedUrl('uid/photo.jpg', expiresIn: 600); - expect(url, contains('xyz')); - }); - }); -} diff --git a/scripts/announce.sh b/scripts/announce.sh index 81e1b397..27086476 100755 --- a/scripts/announce.sh +++ b/scripts/announce.sh @@ -29,9 +29,6 @@ CHANGELOG_ENTRY="## [v$VERSION] - $(date -u +'%Y-%m-%d') - GitHub Actions CI 100% green enforcement - Multi-tenant convention wall (no data leaks) -### Deprecated -- nMedia bundle renamed to ɳTV bundle - For full details, see UPGRADE.md and MIGRATION.md in each repo." # Append to main CHANGELOG (if it exists at project level) diff --git a/scripts/bundle-smoke.sh b/scripts/bundle-smoke.sh index 2baaa1a1..dd639800 100755 --- a/scripts/bundle-smoke.sh +++ b/scripts/bundle-smoke.sh @@ -40,7 +40,6 @@ echo " Checking SDK availability..." echo " → TypeScript: @nself/sdk-ts@$VERSION (npm)" echo " → Python: nself-sdk@$VERSION (PyPI)" echo " → Go: github.com/nself-org/nself-sdk-go@v$VERSION" -echo " → Flutter: nself_sdk@$VERSION (pub.dev)" echo " ℹ Would verify via package manager APIs" # Test 5: Crates.io diff --git a/scripts/release-runbook.md b/scripts/release-runbook.md index 1e0c316c..d3ba0910 100644 --- a/scripts/release-runbook.md +++ b/scripts/release-runbook.md @@ -195,7 +195,7 @@ Deployed: - CLI: homebrew, GitHub Releases - Admin: Docker Hub nself/nself-admin:1.1.1 - Plugins: npm @nself/plugins@1.1.1 - - SDKs: npm, PyPI, crates.io, pub.dev + - SDKs: npm, PyPI, crates.io - Mobile: App Store, Google Play - Web: Vercel (nself.org + subdomains) diff --git a/scripts/sdk-publish.sh b/scripts/sdk-publish.sh index 847032f8..0873fa0e 100755 --- a/scripts/sdk-publish.sh +++ b/scripts/sdk-publish.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # sdk-publish.sh — publish SDKs for v1.1.1 -# Go, Python, TypeScript, Flutter SDKs → language registries +# Go, Python, TypeScript SDKs → language registries set -euo pipefail VERSION="${1:?Usage: $0 }" @@ -36,14 +36,5 @@ else echo " ⚠ cli/sdk/go not found" fi -# Flutter SDK → pub.dev -echo -e "\n[Flutter SDK]" -if [ -f "cli/sdk/flutter/pubspec.yaml" ]; then - echo " nself_sdk v$VERSION" - echo " ℹ flutter pub publish (manual or via CI)" -else - echo " ⚠ cli/sdk/flutter not found" -fi - echo -e "\n=== SDK Publish Complete ===" echo "✅ SDKs ready for publication"