Skip to content

Commit a305fbc

Browse files
committed
feat(api): implement generic data API endpoints with repository support
- Add generic /data and /data/[id] endpoints to handle CRUD operations - Introduce repository pattern for data access with in-memory implementation - Implement model registry for dynamic endpoint configuration - Add error handling and request logging middleware - Update dependencies to include new repository and client libraries
1 parent 9a88889 commit a305fbc

File tree

12 files changed

+800
-47
lines changed

12 files changed

+800
-47
lines changed

lib/src/fixtures/categories.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[
2+
{
3+
"id": "c1a2b3c4-d5e6-f789-0123-456789abcdef",
4+
"name": "Technology",
5+
"description": "News about software development, hardware, and the internet.",
6+
"icon_url": null
7+
},
8+
{
9+
"id": "c2b3c4d5-e6f7-a890-1234-567890abcdef",
10+
"name": "Mobile Development",
11+
"description": "Articles related to mobile app development for iOS and Android.",
12+
"icon_url": null
13+
},
14+
{
15+
"id": "c3d4e5f6-a7b8-c901-d234-e56789abcdef",
16+
"name": "Business",
17+
"description": "News about companies, finance, and the economy.",
18+
"icon_url": null
19+
},
20+
{
21+
"id": "c4e5f6a7-b8c9-d012-e345-f67890abcdef",
22+
"name": "Sports",
23+
"description": "Latest updates from the world of sports.",
24+
"icon_url": null
25+
}
26+
]

lib/src/fixtures/countries.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[
2+
{
3+
"id": "country-us",
4+
"iso_code": "US",
5+
"name": "United States",
6+
"flag_url": "https://example.com/flags/us.png"
7+
},
8+
{
9+
"id": "country-gb",
10+
"iso_code": "GB",
11+
"name": "United Kingdom",
12+
"flag_url": "https://example.com/flags/gb.png"
13+
},
14+
{
15+
"id": "country-ca",
16+
"iso_code": "CA",
17+
"name": "Canada",
18+
"flag_url": "https://example.com/flags/ca.png"
19+
},
20+
{
21+
"id": "country-de",
22+
"iso_code": "DE",
23+
"name": "Germany",
24+
"flag_url": "https://example.com/flags/de.png"
25+
}
26+
]

lib/src/fixtures/headlines.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
[
2+
{
3+
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
4+
"title": "Dart Frog 1.0 Released!",
5+
"description": "The minimalist backend framework for Dart reaches a major milestone.",
6+
"url": "https://dartfrog.vgv.dev/docs/overview",
7+
"imageUrl": null,
8+
"publishedAt": "2025-04-20T10:00:00Z",
9+
"source": {
10+
"id": "s1a2b3c4-d5e6-f789-0123-456789abcdef",
11+
"name": "Very Good Ventures Blog",
12+
"url": "https://vgv.dev/blog",
13+
"type": "blog"
14+
},
15+
"category": {
16+
"id": "c1a2b3c4-d5e6-f789-0123-456789abcdef",
17+
"name": "Technology",
18+
"description": "News about software development and frameworks."
19+
}
20+
},
21+
{
22+
"id": "b2c3d4e5-f6a7-8901-2345-67890abcdef0",
23+
"title": "Flutter Adaptive UI Best Practices",
24+
"description": "Building responsive and adaptive user interfaces in Flutter.",
25+
"url": "https://docs.flutter.dev/ui/layout/adaptive",
26+
"imageUrl": null,
27+
"publishedAt": "2025-04-22T14:30:00Z",
28+
"source": {
29+
"id": "s2b3c4d5-e6f7-a890-1234-567890abcdef",
30+
"name": "Flutter Dev",
31+
"url": "https://flutter.dev",
32+
"type": "specializedPublisher"
33+
},
34+
"category": {
35+
"id": "c2b3c4d5-e6f7-a890-1234-567890abcdef",
36+
"name": "Mobile Development",
37+
"description": "Articles related to mobile app development."
38+
}
39+
}
40+
]

lib/src/fixtures/sources.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[
2+
{
3+
"id": "s1a2b3c4-d5e6-f789-0123-456789abcdef",
4+
"name": "Very Good Ventures Blog",
5+
"description": "Insights from the Very Good Ventures team.",
6+
"url": "https://vgv.dev/blog",
7+
"type": "blog",
8+
"language": "en",
9+
"headquarters": null
10+
},
11+
{
12+
"id": "s2b3c4d5-e6f7-a890-1234-567890abcdef",
13+
"name": "Flutter Dev",
14+
"description": "Official documentation and news for the Flutter framework.",
15+
"url": "https://flutter.dev",
16+
"type": "specializedPublisher",
17+
"language": "en",
18+
"headquarters": null
19+
},
20+
{
21+
"id": "s3c4d5e6-f7a8-b901-c234-d56789abcdef",
22+
"name": "TechCrunch",
23+
"description": "Startup and technology news.",
24+
"url": "https://techcrunch.com/",
25+
"type": "specializedPublisher",
26+
"language": "en",
27+
"headquarters": null
28+
},
29+
{
30+
"id": "s4d5e6f7-a8b9-c012-d345-e67890abcdef",
31+
"name": "BBC News",
32+
"description": "British public service broadcaster.",
33+
"url": "https://www.bbc.com/news",
34+
"type": "nationalNewsOutlet",
35+
"language": "en",
36+
"headquarters": {
37+
"id": "country-gb",
38+
"iso_code": "GB",
39+
"name": "United Kingdom",
40+
"flag_url": "https://example.com/flags/gb.png"
41+
}
42+
}
43+
]
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// ignore_for_file: avoid_catches_without_on_clauses
3+
4+
import 'dart:io';
5+
6+
import 'package:dart_frog/dart_frog.dart';
7+
import 'package:ht_http_client/ht_http_client.dart';
8+
9+
/// Middleware that catches errors and converts them into
10+
/// standardized JSON responses.
11+
Middleware errorHandler() {
12+
return (handler) {
13+
return (context) async {
14+
try {
15+
// Attempt to execute the request handler
16+
final response = await handler(context);
17+
return response;
18+
} on HtHttpException catch (e, stackTrace) {
19+
// Handle specific HtHttpExceptions from the client/repository layers
20+
final statusCode = _mapExceptionToStatusCode(e);
21+
final errorCode = _mapExceptionToCodeString(e);
22+
print('HtHttpException Caught: $e\n$stackTrace'); // Log for debugging
23+
return Response.json(
24+
statusCode: statusCode,
25+
body: {
26+
'error': {
27+
'code': errorCode,
28+
'message': e.message,
29+
},
30+
},
31+
);
32+
} on FormatException catch (e, stackTrace) {
33+
// Handle data format/parsing errors (often indicates bad client input)
34+
print('FormatException Caught: $e\n$stackTrace'); // Log for debugging
35+
return Response.json(
36+
statusCode: HttpStatus.badRequest, // 400
37+
body: {
38+
'error': {
39+
'code': 'INVALID_FORMAT',
40+
'message': 'Invalid data format: ${e.message}',
41+
},
42+
},
43+
);
44+
} catch (e, stackTrace) {
45+
// Handle any other unexpected errors
46+
print('Unhandled Exception Caught: $e\n$stackTrace');
47+
return Response.json(
48+
statusCode: HttpStatus.internalServerError, // 500
49+
body: {
50+
'error': {
51+
'code': 'INTERNAL_SERVER_ERROR',
52+
'message': 'An unexpected internal server error occurred.',
53+
// Avoid leaking sensitive details in production responses
54+
// 'details': e.toString(), // Maybe include in dev mode only
55+
},
56+
},
57+
);
58+
}
59+
};
60+
};
61+
}
62+
63+
/// Maps HtHttpException subtypes to appropriate HTTP status codes.
64+
int _mapExceptionToStatusCode(HtHttpException exception) {
65+
return switch (exception) {
66+
BadRequestException() => HttpStatus.badRequest, // 400
67+
UnauthorizedException() => HttpStatus.unauthorized, // 401
68+
ForbiddenException() => HttpStatus.forbidden, // 403
69+
NotFoundException() => HttpStatus.notFound, // 404
70+
ServerException() => HttpStatus.internalServerError, // 500
71+
NetworkException() => HttpStatus.serviceUnavailable, // 503 (or 500)
72+
UnknownException() => HttpStatus.internalServerError, // 500
73+
};
74+
}
75+
76+
/// Maps HtHttpException subtypes to consistent error code strings.
77+
String _mapExceptionToCodeString(HtHttpException exception) {
78+
return switch (exception) {
79+
BadRequestException() => 'BAD_REQUEST',
80+
UnauthorizedException() => 'UNAUTHORIZED',
81+
ForbiddenException() => 'FORBIDDEN',
82+
NotFoundException() => 'NOT_FOUND',
83+
ServerException() => 'SERVER_ERROR',
84+
NetworkException() => 'NETWORK_ERROR', // Or 'SERVICE_UNAVAILABLE'
85+
UnknownException() => 'UNKNOWN_ERROR',
86+
};
87+
}
Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import 'package:dart_frog/dart_frog.dart';
2-
import 'package:ht_countries_client/ht_countries_client.dart';
3-
import 'package:ht_countries_inmemory/ht_countries_inmemory.dart';
1+
// import 'package:dart_frog/dart_frog.dart';
2+
// import 'package:ht_countries_client/ht_countries_client.dart';
3+
// import 'package:ht_countries_inmemory/ht_countries_inmemory.dart';
44

5-
/// Provides an instance of [HtCountriesClient] to the request context.
6-
///
7-
/// This middleware uses the inmemory implementation
8-
/// [HtCountriesInMemoryClient].
9-
Middleware countriesClientProvider() {
10-
// Create the client instance once when the middleware is initialized.
11-
// This assumes the in-memory client is cheap to create and can be reused
12-
// across requests.
13-
final HtCountriesClient client = HtCountriesInMemoryClient();
5+
// /// Provides an instance of [HtCountriesClient] to the request context.
6+
// ///
7+
// /// This middleware uses the inmemory implementation
8+
// /// [HtCountriesInMemoryClient].
9+
// Middleware countriesClientProvider() {
10+
// // Create the client instance once when the middleware is initialized.
11+
// // This assumes the in-memory client is cheap to create and can be reused
12+
// // across requests.
13+
// final HtCountriesClient client = HtCountriesInMemoryClient();
1414

15-
return (Handler innerHandler) {
16-
return (RequestContext context) {
17-
// Provide the existing client instance to this request's context.
18-
final updatedContext = context.provide<HtCountriesClient>(() => client);
19-
// Call the next handler in the chain with the updated context.
20-
return innerHandler(updatedContext);
21-
};
22-
};
23-
}
15+
// return (Handler innerHandler) {
16+
// return (RequestContext context) {
17+
// // Provide the existing client instance to this request's context.
18+
// final updatedContext = context.provide<HtCountriesClient>(()=> client);
19+
// // Call the next handler in the chain with the updated context.
20+
// return innerHandler(updatedContext);
21+
// };
22+
// };
23+
// }

lib/src/registry/model_registry.dart

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// ignore_for_file: strict_raw_type
3+
4+
import 'package:dart_frog/dart_frog.dart';
5+
import 'package:ht_data_client/ht_data_client.dart';
6+
// HtDataRepository import is no longer needed here
7+
import 'package:ht_shared/ht_shared.dart';
8+
9+
/// {@template model_config}
10+
/// Configuration holder for a specific data model type [T].
11+
///
12+
/// Contains the necessary functions (serialization, ID extraction)
13+
/// required to handle requests for this model type within the
14+
/// generic `/data` endpoint.
15+
/// {@endtemplate}
16+
class ModelConfig<T> {
17+
/// {@macro model_config}
18+
const ModelConfig({
19+
required this.fromJson,
20+
required this.toJson,
21+
required this.getId,
22+
// repositoryProvider removed
23+
});
24+
25+
/// Function to deserialize JSON into an object of type [T].
26+
final FromJson<T> fromJson;
27+
28+
/// Function to serialize an object of type [T] into JSON.
29+
final ToJson<T> toJson;
30+
31+
/// Function to extract the unique string ID from an item of type [T].
32+
final String Function(T item) getId;
33+
34+
// repositoryProvider field removed
35+
}
36+
37+
// Repository providers are no longer defined here.
38+
// They will be created and provided directly in the main dependency setup.
39+
40+
/// {@template model_registry}
41+
/// Central registry mapping model name strings (used in API query params)
42+
/// to their corresponding [ModelConfig] instances.
43+
///
44+
/// This registry is used by the middleware to look up the correct configuration
45+
/// and repository based on the `?model=` query parameter.
46+
/// {@endtemplate}
47+
final modelRegistry = <String, ModelConfig>{
48+
'headline': ModelConfig<Headline>(
49+
fromJson: Headline.fromJson,
50+
toJson: (h) => h.toJson(),
51+
getId: (h) => h.id,
52+
),
53+
'category': ModelConfig<Category>(
54+
fromJson: Category.fromJson,
55+
toJson: (c) => c.toJson(),
56+
getId: (c) => c.id,
57+
),
58+
'source': ModelConfig<Source>(
59+
fromJson: Source.fromJson,
60+
toJson: (s) => s.toJson(),
61+
getId: (s) => s.id,
62+
),
63+
// Add entry for Country model
64+
'country': ModelConfig<Country>(
65+
fromJson: Country.fromJson,
66+
toJson: (c) => c.toJson(),
67+
getId: (c) => c.id, // Assuming Country has an 'id' field
68+
),
69+
};
70+
71+
/// Type alias for the ModelRegistry map for easier provider usage.
72+
typedef ModelRegistryMap = Map<String, ModelConfig>;
73+
74+
/// Dart Frog provider function factory for the entire [modelRegistry].
75+
///
76+
/// This makes the registry available for injection, primarily for the
77+
/// middleware responsible for resolving the model type.
78+
final modelRegistryProvider = provider<ModelRegistryMap>(
79+
(_) => modelRegistry,
80+
); // Use lowercase provider function for setup

pubspec.yaml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,28 @@ environment:
88

99
dependencies:
1010
dart_frog: ^1.1.0
11-
ht_countries_client:
11+
# ht_countries_client:
12+
# git:
13+
# url: https://github.com/headlines-toolkit/ht-countries-client.git
14+
# ht_countries_inmemory:
15+
# git:
16+
# url: https://github.com/headlines-toolkit/ht-countries-inmemory.git
17+
ht_data_client:
1218
git:
13-
url: https://github.com/headlines-toolkit/ht-countries-client.git
14-
ht_countries_inmemory:
19+
url: https://github.com/headlines-toolkit/ht-data-client.git
20+
ht_data_inmemory:
1521
git:
16-
url: https://github.com/headlines-toolkit/ht-countries-inmemory.git
22+
url: https://github.com/headlines-toolkit/ht-data-inmemory.git
23+
ht_data_repository:
24+
git:
25+
url: https://github.com/headlines-toolkit/ht-data-repository.git
26+
ht_http_client:
27+
git:
28+
url: https://github.com/headlines-toolkit/ht-http-client.git
29+
ht_shared:
30+
git:
31+
url: https://github.com/headlines-toolkit/ht-shared.git
32+
1733
meta: ^1.16.0
1834
uuid: ^4.5.1
1935

0 commit comments

Comments
 (0)