diff --git a/apps/vyuh_demo/pubspec.lock b/apps/vyuh_demo/pubspec.lock index 6b4fb7b0..70331415 100644 --- a/apps/vyuh_demo/pubspec.lock +++ b/apps/vyuh_demo/pubspec.lock @@ -171,7 +171,7 @@ packages: path: "../../packages/sanity/flutter_sanity_portable_text" relative: true source: path - version: "1.0.0-beta.8" + version: "1.0.0-beta.20" flutter_shaders: dependency: transitive description: @@ -645,7 +645,7 @@ packages: path: "../../packages/system/vyuh_feature_system" relative: true source: path - version: "1.0.0-beta.4" + version: "1.0.0-beta.5" web: dependency: transitive description: diff --git a/packages/sanity/flutter_sanity_portable_text/example/pubspec.yaml b/packages/sanity/flutter_sanity_portable_text/example/pubspec.yaml index c4d61eaa..c7c98fef 100644 --- a/packages/sanity/flutter_sanity_portable_text/example/pubspec.yaml +++ b/packages/sanity/flutter_sanity_portable_text/example/pubspec.yaml @@ -1,4 +1,4 @@ -name: example +name: flutter_sanity_portable_text_example description: A simple Flutter project to show the usage of flutter_sanity_portable_text publish_to: none version: 1.0.0+1 diff --git a/packages/sanity/sanity_client/CHANGELOG.md b/packages/sanity/sanity_client/CHANGELOG.md index 98b90d74..37f940f5 100644 --- a/packages/sanity/sanity_client/CHANGELOG.md +++ b/packages/sanity/sanity_client/CHANGELOG.md @@ -1,10 +1,16 @@ ## 1.0.0-beta.5 +- Refactored tests for 100% coverage +- Updated pubspec description and readme +- Added documentation for all the classes and methods + ## 1.0.0-beta.4 - - Moved the sanity packages under the vyuh repo +- Moved the sanity packages under the vyuh repo - - **REFACTOR**: moving flutter_sanity_portable_text and sanity_client under vyuh. ([f1175fbd](https://github.com/vyuh-tech/vyuh/commit/f1175fbdb602588ef5f8d978a3d474f15a96e861)) +- **REFACTOR**: moving flutter_sanity_portable_text and sanity_client under + vyuh. + ([f1175fbd](https://github.com/vyuh-tech/vyuh/commit/f1175fbdb602588ef5f8d978a3d474f15a96e861)) ## 1.0.0-beta.3 diff --git a/packages/sanity/sanity_client/README.md b/packages/sanity/sanity_client/README.md index ea24f8b2..d61535fa 100644 --- a/packages/sanity/sanity_client/README.md +++ b/packages/sanity/sanity_client/README.md @@ -8,17 +8,45 @@ https://www.sanity.io/docs/http-query - Connect to a Sanity.io project and run GROQ. - Support all parameters allowed by the HTTP API of Sanity including - `perspective`, `explain`, `useCDN`, etc. -- Has support for switching between URL Builders for images. This is useful if - you are hosting your images on an external service like Cloudinary or ImageKit - -## Getting started - -Use the package by adding to your dependencies. It is still in Beta, so there -will be rough edges for sure.🤞 + `apiVersion`, `perspective`, `explain`, `useCDN`. +- Has support for switching between URL Builders for images and files. This is + useful if you are hosting your images on an external service like _Cloudinary_ + or _ImageKit_. ## Usage -//TODO - -A more comprehensive example will be added along with rich documentation. +Create an instance of the SanityClient and give it a SanityConfig. Use the +`fetch` method to run queries. + +```dart +import 'package:sanity_client/sanity_client.dart'; + +Future main() async { + // using the SanityClient + var client = SanityClient( + SanityConfig( + projectId: 'your_project_id', + dataset: 'your_dataset', + token: 'your_token', + perspective: Perspective.published, + explainQuery: true, + useCdn: true, + apiVersion: 'v2024-02-16', + ), + ); + + // make a query + var query = ''' + *[_type == "movie"]{ + _id, + title, + releaseDate, + "director": crewMembers[job == "Director"][0].person->name + } + '''; + + final response = await client.fetch(query); + print(response); +} + +``` diff --git a/packages/sanity/sanity_client/example/.gitignore b/packages/sanity/sanity_client/example/.gitignore new file mode 100644 index 00000000..3a857904 --- /dev/null +++ b/packages/sanity/sanity_client/example/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/packages/sanity/sanity_client/example/README.md b/packages/sanity/sanity_client/example/README.md new file mode 100644 index 00000000..44aaafe7 --- /dev/null +++ b/packages/sanity/sanity_client/example/README.md @@ -0,0 +1 @@ +# Example for using the Sanity Client diff --git a/packages/sanity/sanity_client/example/lib/example.dart b/packages/sanity/sanity_client/example/lib/example.dart new file mode 100644 index 00000000..8a99a132 --- /dev/null +++ b/packages/sanity/sanity_client/example/lib/example.dart @@ -0,0 +1,29 @@ +import 'package:sanity_client/sanity_client.dart'; + +Future main() async { + // using the SanityClient + var client = SanityClient( + SanityConfig( + projectId: 'your_project_id', + dataset: 'your_dataset', + token: 'your_token', + perspective: Perspective.published, + explainQuery: true, + useCdn: true, + apiVersion: 'v2024-02-16', + ), + ); + + // make a query + var query = ''' + *[_type == "movie"]{ + _id, + title, + releaseDate, + "director": crewMembers[job == "Director"][0].person->name + } + '''; + + final response = await client.fetch(query); + print(response); +} diff --git a/packages/sanity/sanity_client/example/pubspec.lock b/packages/sanity/sanity_client/example/pubspec.lock new file mode 100644 index 00000000..7dadee4b --- /dev/null +++ b/packages/sanity/sanity_client/example/pubspec.lock @@ -0,0 +1,150 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + flutter: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + sanity_client: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "1.0.0-beta.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" +sdks: + dart: ">=3.3.3 <4.0.0" diff --git a/packages/sanity/sanity_client/example/pubspec.yaml b/packages/sanity/sanity_client/example/pubspec.yaml new file mode 100644 index 00000000..f1882c29 --- /dev/null +++ b/packages/sanity/sanity_client/example/pubspec.yaml @@ -0,0 +1,14 @@ +name: sanity_client_example +description: An example for using the sanity_client package +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.3.3 + +# Add regular dependencies here. +dependencies: + sanity_client: ^1.0.0-beta.1 + +dev_dependencies: + lints: ^3.0.0 diff --git a/packages/sanity/sanity_client/lib/client.dart b/packages/sanity/sanity_client/lib/client.dart index ee3ede2c..1fedc32d 100644 --- a/packages/sanity/sanity_client/lib/client.dart +++ b/packages/sanity/sanity_client/lib/client.dart @@ -4,17 +4,33 @@ import 'package:http/http.dart' as http; import 'sanity_client.dart'; +/// The various perspectives that can be used to fetch data from Sanity enum Perspective { raw, previewDrafts, published } +/// Configuration for the Sanity client final class SanityConfig { + /// The dataset to fetch data from final String dataset; + + /// The project id final String projectId; + + /// The token to use for authentication final String token; + + /// Whether to use the CDN or not final bool useCdn; + + /// The API version to use. It follows the format `vYYYY-MM-DD` final String apiVersion; + + /// The perspective to use final Perspective perspective; + + /// Whether to explain the query or not final bool explainQuery; + /// The default API version to use static final String defaultApiVersion = (() { final today = DateTime.now(); final parts = [ @@ -42,12 +58,22 @@ final class SanityConfig { Invalid Token provided. Setup an API token, with Viewer access, in the Sanity Management Console. Without a valid token you will not be able to fetch data from Sanity.'''); + + assert(RegExp(r'^v\d{4}-\d{2}-\d{2}$').hasMatch(this.apiVersion), + 'Invalid API version provided. It should follow the format `vYYYY-MM-DD`'); } } +/// The client for fetching data from Sanity class SanityClient { + /// The configuration for the client final SanityConfig config; + + /// The HTTP client to use. Generally not needed to be provided. + /// It is used for testing purposes final http.Client httpClient; + + /// The URL builder to use. When not provided it uses the default Sanity URL builder final UrlBuilder urlBuilder; final Map _requestHeaders; @@ -60,16 +86,21 @@ class SanityClient { urlBuilder = urlBuilder ?? SanityUrlBuilder(config), _requestHeaders = {'Authorization': 'Bearer ${config.token}'}; - Future fetch(Uri uri) async { + /// Fetches data from Sanity by running the GROQ Query with the passed in parameters + Future fetch(String query, + {Map? params}) async { + final uri = queryUrl(query, params: params); final response = await httpClient.get(uri, headers: _requestHeaders); return _getQueryResult(response); } + /// The URL for the query Uri queryUrl(String query, {Map? params}) => urlBuilder.queryUrl(query, params: params); -//ignore: long-parameter-list + /// Return the associated image url + //ignore: long-parameter-list Uri imageUrl( final String imageRefId, { final int? width, diff --git a/packages/sanity/sanity_client/lib/exceptions.dart b/packages/sanity/sanity_client/lib/exceptions.dart index 33e59370..97e20d45 100644 --- a/packages/sanity/sanity_client/lib/exceptions.dart +++ b/packages/sanity/sanity_client/lib/exceptions.dart @@ -1,4 +1,5 @@ -class SanityException implements Exception { +/// Base type for exceptions thrown by the Sanity client. +abstract class SanityException implements Exception { SanityException([ this._message, this._prefix, @@ -11,36 +12,27 @@ class SanityException implements Exception { String toString() => '${_prefix ?? ''}${_message ?? ''}'; } +/// Exception thrown when a document is not found. class FetchDataException extends SanityException { FetchDataException([final String? message]) : super(message, 'Error during communication: '); } +/// Exception when the request is invalid. class BadRequestException extends SanityException { BadRequestException([final String? message]) : super(message, 'Invalid request: '); } +/// Exception when the request is unauthorized and does not include a valid token +/// in the Authorization header. class UnauthorizedException extends SanityException { UnauthorizedException([final String? message]) : super(message, 'Unauthorized: '); } +/// Exception when the request is forbidden. class InvalidReferenceException extends SanityException { InvalidReferenceException([final String? message]) : super(message, 'Invalid reference: '); } - -final class InvalidResultTypeException extends SanityException { - final Type expectedType; - final Type actualType; - - InvalidResultTypeException({ - required this.expectedType, - required this.actualType, - }); - - @override - String toString() => - 'Invalid result type: $actualType. Was expecting $expectedType'; -} diff --git a/packages/sanity/sanity_client/lib/response_types.dart b/packages/sanity/sanity_client/lib/response_types.dart index c8f3de3c..469d5ed8 100644 --- a/packages/sanity/sanity_client/lib/response_types.dart +++ b/packages/sanity/sanity_client/lib/response_types.dart @@ -2,9 +2,13 @@ import 'package:json_annotation/json_annotation.dart'; part 'response_types.g.dart'; +/// A class to represent a Sanity dataset. @JsonSerializable(createToJson: false) class SanityDataset { + /// The name of the dataset. final String name; + + /// The ACL mode of the dataset. final String aclMode; SanityDataset({required this.name, required this.aclMode}); @@ -21,10 +25,16 @@ class SanityDataset { int get hashCode => (name + aclMode).hashCode; } +/// A class to represent a Sanity response to a query. @JsonSerializable() class ServerResponse { + /// The result of the query, which can be a null, object or array. final Map? result; + + /// The time it took for the server to respond to the query. final int ms; + + /// The query that was sent to the server. final String query; ServerResponse({ @@ -37,11 +47,22 @@ class ServerResponse { _$ServerResponseFromJson(json); } +/// A class that extracts various performance information from a Sanity response. final class PerformanceInfo { + /// The query that was sent to the server. final String query; + + /// The time it took for the server to respond to the query. final int serverTimeMs; + + /// The time it took for the client to process the response. This is the + /// complete round-trip time. final int clientTimeMs; + + /// The shard that the query was sent to. final String shard; + + /// The age of the data that was returned in the response. final int age; PerformanceInfo({ @@ -53,8 +74,12 @@ final class PerformanceInfo { }); } +/// A class that represents a Sanity query response. final class SanityQueryResponse { + /// The result of the query, which can be a null, object or array. final dynamic result; + + /// The performance information of the query. final PerformanceInfo info; SanityQueryResponse({required this.result, required this.info}); diff --git a/packages/sanity/sanity_client/lib/sanity_client.dart b/packages/sanity/sanity_client/lib/sanity_client.dart index 4277cdd3..4274dd3e 100644 --- a/packages/sanity/sanity_client/lib/sanity_client.dart +++ b/packages/sanity/sanity_client/lib/sanity_client.dart @@ -1,3 +1,6 @@ +/// A Native Dart client to connect to Sanity.io and run GROQ queries. +/// +/// It supports all parameters of the query API. library sanity_client; export 'client.dart'; diff --git a/packages/sanity/sanity_client/lib/url_builder.dart b/packages/sanity/sanity_client/lib/url_builder.dart index fb5348dd..9cef54a6 100644 --- a/packages/sanity/sanity_client/lib/url_builder.dart +++ b/packages/sanity/sanity_client/lib/url_builder.dart @@ -2,33 +2,58 @@ import 'dart:math'; import 'package:sanity_client/sanity_client.dart'; +/// Options to customize the image URL. +/// +/// This class is used to customize the image URL when fetching images from Sanity. +/// You can provide various parameter such as [width], [height], [quality], [devicePixelRatio], [format], +/// which will then be used to generate the image URL. class ImageOptions { + /// The width of the image in pixels. final int? width; + + /// The height of the image in pixels. final int? height; + + /// The device pixel ratio of the image. final int? devicePixelRatio; + + /// The quality of the image, expressed as value between 0-100 final int? quality; + + /// The format of the image. Can be either `jpg`, `png`, `webp`, or `auto`. final String? format; - ImageOptions( - {this.width, - this.height, - this.devicePixelRatio, - this.quality, - this.format}); + ImageOptions({ + this.width, + this.height, + this.devicePixelRatio, + this.quality, + this.format, + }); } +/// Provides the main interface for building URLs for Sanity assets. +/// +/// This class is used to build URLs for Sanity assets such as files, images, and queries. abstract class UrlBuilder { + /// The configuration object for the client, which is specific to the URL builder implementation. final TConfig config; UrlBuilder(this.config); + /// Builds a URL for a file asset. Uri fileUrl(String fileRefId); + /// Builds a URL for an image asset. Uri imageUrl(String imageRefId, {ImageOptions? options}); + /// Builds a URL for a GROQ query. Uri queryUrl(String query, {Map? params}); } +/// A URL builder implementation for Sanity. +/// +/// Supports building URLs for files, images, and queries. final class SanityUrlBuilder extends UrlBuilder { SanityUrlBuilder(super.config); @@ -92,6 +117,7 @@ final class SanityUrlBuilder extends UrlBuilder { } } +/// Internal class to parse image reference IDs. class _ParsedReference { final String id; final int width; diff --git a/packages/sanity/sanity_client/test/query_test.dart b/packages/sanity/sanity_client/test/query_test.dart index da379cb8..8c9688ba 100644 --- a/packages/sanity/sanity_client/test/query_test.dart +++ b/packages/sanity/sanity_client/test/query_test.dart @@ -30,6 +30,29 @@ void main() { expect(client.urlBuilder, isA()); }); + test('Asserts that a token is provided', () async { + expect( + () => SanityConfig( + projectId: project, + dataset: dataset, + token: '', + ), + throwsA(isA()), + ); + }); + + test('Asserts that the apiVersion has a valid format', () async { + expect( + () => SanityConfig( + projectId: project, + dataset: dataset, + token: token, + apiVersion: 'invalid', + ), + throwsA(isA()), + ); + }); + test('Request is constructed properly', () async { final httpClient = MockClient((final req) async { expect(req.url.host, contains('$project.apicdn.sanity.io')); @@ -45,7 +68,7 @@ void main() { final c1 = getClient(httpClient: httpClient); - await c1.fetch(c1.queryUrl('some query')); + await c1.fetch('some query'); }); test('Params are being sent properly', () async { @@ -53,7 +76,7 @@ void main() { expect(req.url.query, contains('test=true')); expect(req.url.query, contains('explain=true')); expect(req.url.toString(), contains('api.sanity.io')); - expect(req.url.path, contains('v12345')); + expect(req.url.path, contains('v2024-02-16')); return http.Response(_sanityResponse, 200); }); @@ -63,10 +86,10 @@ void main() { explainQuery: true, perspective: Perspective.previewDrafts, useCdn: false, - apiVersion: 'v12345', + apiVersion: 'v2024-02-16', ); - await c1.fetch(c1.queryUrl('some query', params: {'test': 'true'})); + await c1.fetch('some query', params: {'test': 'true'}); }); test('Correct domain is used when CDN is false', () async { @@ -84,7 +107,7 @@ void main() { httpClient: httpClient, ); - await c1.fetch(c1.queryUrl('some query')); + await c1.fetch('some query'); }); group('Throws an exception for invalid conditions', () { @@ -122,7 +145,7 @@ void main() { test('Throws for $key', () { expect( () async { - await client.fetch(client.queryUrl(key)); + await client.fetch(key); }, throwsA(value.$2), ); @@ -134,7 +157,7 @@ void main() { var message = ''; try { final client = getClient(); - await client.fetch(client.queryUrl('Unauthorized 403')); + await client.fetch('Unauthorized 403'); } catch (e) { message = e.toString(); } @@ -160,7 +183,7 @@ void main() { final client = getClient(httpClient: httpClient); - final response = await client.fetch(client.queryUrl('valid query')); + final response = await client.fetch('valid query'); expect(response.result, equals({})); expect(response.info.serverTimeMs, equals(20)); });