diff --git a/README.md b/README.md
index 4a44095..2d6bacf 100644
--- a/README.md
+++ b/README.md
@@ -78,9 +78,10 @@ When utilizing the `nitric new` command to initiate a new project, the available
### Dart
-| Name | Description | Features |
-| ---------------------------------- | ---------------- | -------- |
-| [dart-starter](./v1/dart-starter/) | REST API Starter | APIs |
+| Name | Description | Features |
+| ---------------------------------- | ----------------------------------------------- | ---------------------- |
+| [dart-starter](./v1/dart-starter/) | REST API Starter | APIs |
+| [flutter](./v1/flutter/) | Basic Flutter Application with a Nitric backend | APIs, Key Value Stores |
### Go
diff --git a/v1/flutter/.gitignore b/v1/flutter/.gitignore
new file mode 100644
index 0000000..29a3a50
--- /dev/null
+++ b/v1/flutter/.gitignore
@@ -0,0 +1,43 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/v1/flutter/.metadata b/v1/flutter/.metadata
new file mode 100644
index 0000000..9d32c61
--- /dev/null
+++ b/v1/flutter/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "b0850beeb25f6d5b10426284f506557f66181b36"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ - platform: android
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ - platform: ios
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ - platform: linux
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ - platform: macos
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ - platform: web
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ - platform: windows
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/v1/flutter/README.md b/v1/flutter/README.md
new file mode 100644
index 0000000..775117a
--- /dev/null
+++ b/v1/flutter/README.md
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+ A fast & fun way to build portable cloud-native applications
+
+
+
+
+
+
+
+
+
+
+## Project Description
+
+A basic Flutter application that uses Nitric as the backend service for the API and key value collections.
+
+## Running this project
+
+To run this project you'll need the [Nitric CLI](https://nitric.io/docs/installation) installed, then you can use the CLI commands to run, build or deploy the project.
+
+You'll also want to make sure the project's required dependencies have been installed.
+
+```bash
+# install dependencies
+dart pub get
+
+# run locally
+nitric start
+```
+
+## About Nitric
+
+[Nitric](https://nitric.io) is a framework for rapid development of cloud-native and serverless applications. Define your apps in terms of the resources they need, then write the code for serverless function based APIs, event subscribers and scheduled jobs.
+
+Apps built with Nitric can be deployed to AWS, Azure or Google Cloud all from the same code base so you can focus on your products, not your cloud provider.
+
+Nitric makes it easy to:
+
+- Create smart serverless functions and APIs
+- Build reliable distributed apps that use events and/or queues
+- Securely store and retrieve secrets
+- Read and write files from buckets
+
+## Documentation
+
+The full documentation is available at [nitric.io/docs](https://nitric.io/docs).
+
+We're completely open-source and encourage [code contributions](https://nitric.io/docs/contributions).
+
+## Get in touch
+
+- Jump into our [Discord server](https://nitric.io/chat)
+
+- Ask questions in [GitHub discussions](https://github.com/nitrictech/nitric/discussions)
+
+- Find us on [Twitter](https://twitter.com/nitric_io)
+
+- Send us an [email](mailto:maintainers@nitric.io)
diff --git a/v1/flutter/analysis_options.yaml b/v1/flutter/analysis_options.yaml
new file mode 100644
index 0000000..0d29021
--- /dev/null
+++ b/v1/flutter/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/v1/flutter/docker/flutter.dockerfile b/v1/flutter/docker/flutter.dockerfile
new file mode 100644
index 0000000..fafdfae
--- /dev/null
+++ b/v1/flutter/docker/flutter.dockerfile
@@ -0,0 +1,40 @@
+FROM dart:stable AS build
+
+# The Nitric CLI will provide the HANDLER arg with the location of our service
+ARG HANDLER
+WORKDIR /app
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+# download Flutter SDK from Flutter Github repo
+RUN git clone https://github.com/flutter/flutter.git /usr/local/flutter
+
+ENV DEBIAN_FRONTEND=dialog
+
+# Set flutter environment path
+ENV PATH="/usr/local/flutter/bin:/usr/local/flutter/bin/cache/dart-sdk/bin:${PATH}"
+
+# Run flutter doctor
+RUN flutter doctor
+
+# Resolve app dependencies.
+COPY pubspec.* ./
+RUN flutter pub get
+
+# Ensure the ./bin folder exists
+RUN mkdir -p ./bin
+
+# Copy app source code and AOT compile it.
+COPY . .
+# Ensure packages are still up-to-date if anything has changed
+RUN flutter pub get --offline
+# Compile the dart service into an exe
+RUN dart compile exe ./${HANDLER} -o bin/main
+
+# Start from scratch and copy in the necessary runtime files
+FROM alpine
+
+COPY --from=build /runtime/ /
+COPY --from=build /app/bin/main /app/bin/
+
+ENTRYPOINT ["/app/bin/main"]
\ No newline at end of file
diff --git a/v1/flutter/docker/flutter.dockerignore b/v1/flutter/docker/flutter.dockerignore
new file mode 100644
index 0000000..6f67a3d
--- /dev/null
+++ b/v1/flutter/docker/flutter.dockerignore
@@ -0,0 +1,13 @@
+build
+test
+.nitric
+.idea
+.dart_tool
+.git
+docker
+android
+ios
+linux
+macos
+web
+windows
\ No newline at end of file
diff --git a/v1/flutter/lib/cors.dart b/v1/flutter/lib/cors.dart
new file mode 100644
index 0000000..638744b
--- /dev/null
+++ b/v1/flutter/lib/cors.dart
@@ -0,0 +1,23 @@
+import 'package:nitric_sdk/nitric.dart';
+
+/// Handle Preflight Options requests by returning status 200 to the requests
+Future optionsHandler(HttpContext ctx) async {
+ ctx.res.headers["Content-Type"] = ["text/html; charset=ascii"];
+ ctx.res.body = "OK";
+
+ return ctx.next();
+}
+
+/// Add CORS headers to responses
+Future addCors(HttpContext ctx) async {
+ ctx.res.headers["Access-Control-Allow-Origin"] = ["*"];
+ ctx.res.headers["Access-Control-Allow-Headers"] = [
+ "Origin, X-Requested-With, Content-Type, Accept, Authorization",
+ ];
+ ctx.res.headers["Access-Control-Allow-Methods"] = [
+ "GET, PUT, POST, PATCH, OPTIONS, DELETE",
+ ];
+ ctx.res.headers["Access-Control-Max-Age"] = ["7200"];
+
+ return ctx.next();
+}
diff --git a/v1/flutter/lib/favorite.dart b/v1/flutter/lib/favorite.dart
new file mode 100644
index 0000000..62470f7
--- /dev/null
+++ b/v1/flutter/lib/favorite.dart
@@ -0,0 +1,13 @@
+class Favorite {
+ /// The name of the favorite
+ String name;
+
+ Favorite(this.name);
+
+ /// Convert a json decodable map to a Favorite object
+ Favorite.fromJson(Map json) : name = json['name'];
+
+ /// Convert a Favorite object to a json encodable
+ static Map toJson(Favorite favorite) =>
+ {'name': favorite.name};
+}
diff --git a/v1/flutter/lib/main.dart b/v1/flutter/lib/main.dart
new file mode 100644
index 0000000..51a456e
--- /dev/null
+++ b/v1/flutter/lib/main.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:word_generator/pages/home.dart';
+import 'package:word_generator/providers/favorites.dart';
+import 'package:word_generator/providers/word.dart';
+
+void main() => runApp(Application());
+
+class Application extends StatelessWidget {
+ const Application({super.key});
+ @override
+ Widget build(BuildContext context) {
+ return MultiProvider(
+ providers: [
+ ChangeNotifierProvider(create: (context) => FavoritesProvider()),
+ ChangeNotifierProvider(create: (context) => WordProvider()),
+ ],
+ child: MaterialApp(
+ title: 'Word Generator App',
+ theme: ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
+ ),
+ home: HomePage(), // <-- Change here
+ ),
+ );
+ }
+}
diff --git a/v1/flutter/lib/pages/favorites.dart b/v1/flutter/lib/pages/favorites.dart
new file mode 100644
index 0000000..80e599c
--- /dev/null
+++ b/v1/flutter/lib/pages/favorites.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:word_generator/providers/favorites.dart';
+
+class FavoritesPage extends StatelessWidget {
+ const FavoritesPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ var favorites = context.watch();
+
+ // If the favorites list is still loading then show a spinning circle.
+ if (favorites.isLoading) {
+ return const Center(
+ child: SizedBox(
+ width: 40,
+ height: 40,
+ child: CircularProgressIndicator(color: Colors.blue),
+ ));
+ }
+
+ // Otherwise return a list of all the favorites
+ return ListView(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(20),
+ // Display how many favorites there are
+ child: Text('You have '
+ '${favorites.favorites.length} favorites:'),
+ ),
+ // Create a list tile for every favorite in the list of favorites
+ for (var favorite in favorites.favorites)
+ ListTile(
+ leading: Icon(Icons.favorite), // <- A heart icon
+ title: Text(favorite.name),
+ ),
+ ],
+ );
+ }
+}
diff --git a/v1/flutter/lib/pages/generator.dart b/v1/flutter/lib/pages/generator.dart
new file mode 100644
index 0000000..3b48167
--- /dev/null
+++ b/v1/flutter/lib/pages/generator.dart
@@ -0,0 +1,147 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:word_generator/providers/favorites.dart';
+import 'package:word_generator/providers/word.dart';
+
+class GeneratorPage extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
+ final style = theme.textTheme.displayMedium!.copyWith(
+ color: theme.colorScheme.onPrimary,
+ );
+
+ final favorites = context.watch();
+ final words = context.watch();
+
+ IconData icon = Icons.favorite_border;
+
+ if (favorites.hasFavorite(words.current)) {
+ icon = Icons.favorite;
+ }
+
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Expanded(
+ // <- allows the list to extend to the top of the page
+ flex: 3,
+ child: HistoryListView(), // <- Add the history list view here
+ ),
+ const SizedBox(height: 10),
+ Card(
+ color: theme.colorScheme.primary,
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: AnimatedSize(
+ duration: const Duration(milliseconds: 200),
+ child: MergeSemantics(
+ child: Wrap(
+ children: [
+ Text(
+ words.current.first,
+ style: style.copyWith(fontWeight: FontWeight.w200),
+ ),
+ Text(
+ words.current.second,
+ style: style.copyWith(fontWeight: FontWeight.bold),
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(height: 10),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ElevatedButton.icon(
+ onPressed: () {
+ favorites.toggleFavorite(words.current);
+ },
+ icon: Icon(icon),
+ label: const Text('Like'),
+ ),
+ const SizedBox(width: 10),
+ ElevatedButton(
+ onPressed: () {
+ words.getNext();
+ },
+ child: const Text('Next'),
+ ),
+ ],
+ ),
+ const Spacer(flex: 2),
+ ],
+ ),
+ );
+ }
+}
+
+class HistoryListView extends StatefulWidget {
+ const HistoryListView({super.key});
+
+ @override
+ State createState() => _HistoryListViewState();
+}
+
+class _HistoryListViewState extends State {
+ final _key = GlobalKey();
+
+ // Create a mask which will make a fade out appearance by making a linear gradient of transparent -> opaque.
+ static const Gradient _maskingGradient = LinearGradient(
+ colors: [Colors.transparent, Colors.black],
+ stops: [0.0, 0.5],
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ );
+
+ @override
+ Widget build(BuildContext context) {
+ final favorites = context.watch();
+ final words = context.watch();
+
+ // Set the key of the animated list to the WordProvider GlobalKey so it can be manipulated from there
+ // Not recommended for a production app as it can slow performance...
+ // Read more here: https://api.flutter.dev/flutter/widgets/GlobalKey-class.html
+ words.historyListKey = _key;
+
+ return ShaderMask(
+ shaderCallback: (bounds) => _maskingGradient.createShader(bounds),
+ // This blend mode takes the opacity of the shader (i.e. our gradient)
+ // and applies it to the destination (i.e. our animated list).
+ blendMode: BlendMode.dstIn,
+ child: AnimatedList(
+ key: _key,
+ // Reverse the list so the latest is on the bottom
+ reverse: true,
+ padding: const EdgeInsets.only(top: 100),
+ initialItemCount: words.history.length,
+ // Build each item in the list, will be run initially and when a new word pair is added.
+ itemBuilder: (context, index, animation) {
+ final pair = words.history[index];
+ return SizeTransition(
+ sizeFactor: animation,
+ child: Center(
+ child: TextButton.icon(
+ onPressed: () {
+ favorites.toggleFavorite(pair);
+ },
+ // If the word pair was favorited, show a heart next to it
+ icon: favorites.hasFavorite(pair)
+ ? const Icon(Icons.favorite, size: 12)
+ : const SizedBox(),
+ label: Text(
+ pair.asLowerCase,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/v1/flutter/lib/pages/home.dart b/v1/flutter/lib/pages/home.dart
new file mode 100644
index 0000000..ca44b81
--- /dev/null
+++ b/v1/flutter/lib/pages/home.dart
@@ -0,0 +1,104 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:word_generator/providers/favorites.dart';
+import 'favorites.dart';
+import 'generator.dart';
+
+class HomePage extends StatefulWidget {
+ const HomePage({super.key});
+
+ @override
+ State createState() => _HomePageState();
+}
+
+class _HomePageState extends State {
+ var selectedIndex = 0;
+ @override
+ void initState() {
+ super.initState();
+ context.read().fetchData();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var colorScheme = Theme.of(context).colorScheme;
+ Widget page;
+ switch (selectedIndex) {
+ case 0:
+ page = GeneratorPage();
+ case 1:
+ page = FavoritesPage();
+ default:
+ throw UnimplementedError('no widget for $selectedIndex');
+ }
+ // The container for the current page, with its background color
+ // and subtle switching animation.
+ var mainArea = ColoredBox(
+ color: colorScheme.surfaceContainerHighest,
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 200),
+ child: page,
+ ),
+ );
+ return Scaffold(
+ body: LayoutBuilder(
+ builder: (context, constraints) {
+ if (constraints.maxWidth < 450) {
+ return Column(
+ children: [
+ Expanded(child: mainArea),
+ SafeArea(
+ child: BottomNavigationBar(
+ items: const [
+ BottomNavigationBarItem(
+ icon: Icon(Icons.home),
+ label: 'Home',
+ ),
+ BottomNavigationBarItem(
+ icon: Icon(Icons.favorite),
+ label: 'Favorites',
+ ),
+ ],
+ currentIndex: selectedIndex,
+ onTap: (value) {
+ setState(() {
+ selectedIndex = value;
+ });
+ },
+ ),
+ )
+ ],
+ );
+ } else {
+ return Row(
+ children: [
+ SafeArea(
+ child: NavigationRail(
+ extended: constraints.maxWidth >= 600,
+ destinations: [
+ NavigationRailDestination(
+ icon: Icon(Icons.home),
+ label: Text('Home'),
+ ),
+ NavigationRailDestination(
+ icon: Icon(Icons.favorite),
+ label: Text('Favorites'),
+ ),
+ ],
+ selectedIndex: selectedIndex,
+ onDestinationSelected: (value) {
+ setState(() {
+ selectedIndex = value;
+ });
+ },
+ ),
+ ),
+ Expanded(child: mainArea),
+ ],
+ );
+ }
+ },
+ ),
+ );
+ }
+}
diff --git a/v1/flutter/lib/providers/favorites.dart b/v1/flutter/lib/providers/favorites.dart
new file mode 100644
index 0000000..aa6ec2c
--- /dev/null
+++ b/v1/flutter/lib/providers/favorites.dart
@@ -0,0 +1,78 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:http/http.dart' as http;
+import 'package:english_words/english_words.dart';
+import 'package:word_generator/favorite.dart';
+
+class FavoritesProvider extends ChangeNotifier {
+ final baseApiUrl = "http://localhost:4001";
+
+ List _favorites = [];
+ bool _isLoading = false;
+
+ /// Get a list of active favorite
+ List get favorites => _favorites;
+
+ /// Check whether the data is loading or not
+ bool get isLoading => _isLoading;
+
+ /// Updates the list of favorites whilst returning a Future with the list of favorites.
+ /// Sets isLoading to true when the favorites have been fetched
+ Future> fetchData() async {
+ _isLoading = true;
+ notifyListeners();
+
+ final response = await http.get(Uri.parse("$baseApiUrl/favorites"));
+
+ if (response.statusCode == 200) {
+ // Decode the json data into an iterable list of unknown objects
+ Iterable rawFavorites = jsonDecode(response.body);
+
+ // Map over the iterable, converting it to a list of Favorite objects
+ _favorites = List.from(
+ rawFavorites.map((model) => Favorite.fromJson(model)));
+ } else {
+ throw Exception('Failed to load data');
+ }
+
+ _isLoading = false;
+ notifyListeners();
+
+ return _favorites;
+ }
+
+ bool hasFavorite(WordPair pair) {
+ if (isLoading) {
+ return false;
+ }
+
+ return _favorites.any((f) => f.name == pair.asLowerCase);
+ }
+
+ /// Toggles whether a favorite being liked or unliked.
+ Future toggleFavorite(WordPair pair) async {
+ // Convert the word pair into a json encoded
+ final encodedFavorites =
+ jsonEncode(Favorite.toJson(Favorite(pair.asLowerCase)));
+
+ // Makes a post request to the toggle favorite route.
+ final response = await http.post(Uri.parse("$baseApiUrl/favorite"),
+ body: encodedFavorites);
+
+ // If the response doesn't respond with OK, throw an error
+ if (response.statusCode != 200) {
+ throw Exception("Failed to add favorite: ${response.body}");
+ }
+
+ // If it was successfully removed update favorites
+ if (hasFavorite(pair)) {
+ // Remove the favorite for
+ _favorites.removeWhere((f) => f.name == pair.asLowerCase);
+ } else {
+ _favorites.add(Favorite(pair.asLowerCase));
+ }
+
+ notifyListeners();
+ }
+}
diff --git a/v1/flutter/lib/providers/word.dart b/v1/flutter/lib/providers/word.dart
new file mode 100644
index 0000000..e542235
--- /dev/null
+++ b/v1/flutter/lib/providers/word.dart
@@ -0,0 +1,25 @@
+import 'package:english_words/english_words.dart';
+import 'package:flutter/material.dart';
+
+class WordProvider extends ChangeNotifier {
+ // The current word pair
+ var current = WordPair.random();
+ // A list of all generated word pairs
+ var history = [];
+
+ // A key that is used to get a reference to the history list state
+ GlobalKey? historyListKey;
+
+ // Generate a new word pair and notify the listeners
+ void getNext() {
+ // Add the current pair to the start of the history list
+ history.insert(0, current);
+
+ // Adds space to the start of the animated list and triggers an animation to start
+ var animatedList = historyListKey?.currentState as AnimatedListState?;
+ animatedList?.insertItem(0);
+
+ current = WordPair.random();
+ notifyListeners();
+ }
+}
diff --git a/v1/flutter/lib/services/main.dart b/v1/flutter/lib/services/main.dart
new file mode 100644
index 0000000..8af4b29
--- /dev/null
+++ b/v1/flutter/lib/services/main.dart
@@ -0,0 +1,58 @@
+import 'dart:convert';
+import 'package:word_generator/cors.dart';
+import 'package:word_generator/favorite.dart';
+
+import 'package:nitric_sdk/nitric.dart';
+
+void main() async {
+ final api = Nitric.api("main", opts: ApiOptions(middlewares: [addCors]));
+
+ final favoritesKV = Nitric.kv("favorites").allow([
+ KeyValueStorePermission.get,
+ KeyValueStorePermission.set,
+ KeyValueStorePermission.delete
+ ]);
+
+ api.options("/favorites", optionsHandler);
+ api.get("/favorites", (ctx) async {
+ var keys = await favoritesKV.keys();
+
+ var favorites = await keys.asyncMap((k) async {
+ final favorite = await favoritesKV.get(k);
+
+ return favorite;
+ }).toList();
+
+ ctx.res.body = jsonEncode(favorites);
+
+ return ctx;
+ });
+
+ api.options("/favorite", optionsHandler);
+ api.post("/favorite", (ctx) async {
+ final req = ctx.req.json();
+
+ final favorite = Favorite.fromJson(req);
+
+ var exists = false;
+
+ final keys = await favoritesKV.keys(prefix: favorite.name);
+
+ await for (final key in keys) {
+ if (key == favorite.name) {
+ exists = true;
+ }
+ }
+
+ // if it exists delete and return
+ if (exists) {
+ await favoritesKV.delete(favorite.name);
+
+ return ctx;
+ }
+
+ await favoritesKV.set(favorite.name, Favorite.toJson(favorite));
+
+ return ctx;
+ });
+}
diff --git a/v1/flutter/nitric-spec.json b/v1/flutter/nitric-spec.json
new file mode 100644
index 0000000..42da14a
--- /dev/null
+++ b/v1/flutter/nitric-spec.json
@@ -0,0 +1,61 @@
+{
+ "resources": [
+ {
+ "id": {
+ "name": "main"
+ },
+ "api": {
+ "openapi": "{\"components\":{},\"info\":{\"title\":\"main\",\"version\":\"v1\"},\"openapi\":\"3.0.1\",\"paths\":{\"/favourite\":{\"options\":{\"operationId\":\"favouriteoptions\",\"responses\":{\"default\":{\"description\":\"\"}},\"security\":[],\"x-nitric-target\":{\"name\":\"backend_lib-services-main\",\"type\":\"function\"}},\"post\":{\"operationId\":\"favouritepost\",\"responses\":{\"default\":{\"description\":\"\"}},\"security\":[],\"x-nitric-target\":{\"name\":\"backend_lib-services-main\",\"type\":\"function\"}}},\"/favourites\":{\"get\":{\"operationId\":\"favouritesget\",\"responses\":{\"default\":{\"description\":\"\"}},\"security\":[],\"x-nitric-target\":{\"name\":\"backend_lib-services-main\",\"type\":\"function\"}},\"options\":{\"operationId\":\"favouritesoptions\",\"responses\":{\"default\":{\"description\":\"\"}},\"security\":[],\"x-nitric-target\":{\"name\":\"backend_lib-services-main\",\"type\":\"function\"}}}}}"
+ }
+ },
+ {
+ "id": {
+ "type": "KeyValueStore",
+ "name": "favourites"
+ },
+ "keyValueStore": {}
+ },
+ {
+ "id": {
+ "type": "Policy",
+ "name": "4e19479ef94af74f894d83df7315494c"
+ },
+ "policy": {
+ "principals": [
+ {
+ "id": {
+ "type": "Service",
+ "name": "backend_lib-services-main"
+ }
+ }
+ ],
+ "actions": [
+ "KeyValueStoreRead",
+ "KeyValueStoreWrite",
+ "KeyValueStoreDelete"
+ ],
+ "resources": [
+ {
+ "id": {
+ "type": "KeyValueStore",
+ "name": "favourites"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "id": {
+ "type": "Service",
+ "name": "backend_lib-services-main"
+ },
+ "service": {
+ "image": {
+ "uri": "backend_lib-services-main"
+ },
+ "workers": 1,
+ "type": "default"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/v1/flutter/nitric.dev.yaml b/v1/flutter/nitric.dev.yaml
new file mode 100755
index 0000000..cbae95e
--- /dev/null
+++ b/v1/flutter/nitric.dev.yaml
@@ -0,0 +1,68 @@
+# The nitric provider to use
+provider: nitric/aws@1.11.1
+# The target aws region to deploy to
+# See available regions:
+# https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
+region: us-east-1
+# Optional Configuration Below
+
+# The timezone that deployed schedules will run with
+# Format is in tz identifiers:
+# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+# schedule-timezone: Australia/Sydney # Available since v0.27.0
+
+# Import existing AWS Resources
+# Currently only secrets are supported
+# Available since v0.28.0
+# import:
+# # A name ARN map of secrets, where the name matches the nitric name of the secret you would like to import
+# secrets: # Available since v0.28.0
+# # In typescript this would import the provided secret reference for a secret declared as
+# # const mySecret = secret('my-secret');
+# my-secret: arn:...
+
+# # Apply configuration to nitric APIs
+# apis:
+# # The nitric name of the API to configure
+# my-api:
+# # Array of domains to apply to the API
+# # The domain or parent domain must have a hosted zone already in Route53
+# domains:
+# - api.example.com
+
+# # Configure your deployed functions/services
+# config:
+# # How functions without a type will be deployed
+# default:
+# # configure a sample rate for telemetry (between 0 and 1) e.g. 0.5 is 50%
+# telemetry: 0
+# # configure functions to deploy to AWS lambda
+# lambda: # Available since v0.26.0
+# # set 128MB of RAM
+# # See lambda configuration docs here:
+# # https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-common.html#configuration-memory-console
+# memory: 128
+# # set a timeout of 15 seconds
+# # See lambda timeout values here:
+# # https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-common.html#configuration-timeout-console
+# timeout: 15
+# # set a provisioned concurrency value
+# # For info on provisioned concurrency for AWS Lambda see:
+# # https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html
+# provisioned-concurrency: 0
+# # Configure VPCs that the lambda can access
+# vpc:
+# # Array of existing security group ids to apply
+# security-group-ids:
+# - sg-xxx
+# # Array of existing subnet ids to apply
+# subnet-ids:
+# - subnet-xxx
+# # Additional deployment types
+# # You can target these types by setting a `type` in your project configuration
+# big-service:
+# telemetry: 0
+# lambda:
+# memory: 1024
+# timeout: 60
+# provisioned-concurrency: 1
diff --git a/v1/flutter/nitric.yaml b/v1/flutter/nitric.yaml
new file mode 100644
index 0000000..c3f15fc
--- /dev/null
+++ b/v1/flutter/nitric.yaml
@@ -0,0 +1,9 @@
+name: backend
+services:
+ - match: lib/services/*.dart
+ runtime: flutter
+ start: dart run $SERVICE_PATH
+runtimes:
+ flutter:
+ dockerfile: ./docker/flutter.dockerfile
+ args: {}
diff --git a/v1/flutter/pubspec.yaml b/v1/flutter/pubspec.yaml
new file mode 100644
index 0000000..7fde40a
--- /dev/null
+++ b/v1/flutter/pubspec.yaml
@@ -0,0 +1,92 @@
+name: word_generator
+description: "A new Flutter project."
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: "none" # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+# In Windows, build-name is used as the major, minor, and patch parts
+# of the product and file versions while build-number is used as the build suffix.
+version: 1.0.0+1
+
+environment:
+ sdk: ">=3.4.4 <4.0.0"
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.6
+ english_words: ^4.0.0
+ http: ^1.2.2
+ provider: ^6.1.2
+ nitric_sdk: ^1.4.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: ^3.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/custom-fonts/#from-packages