diff --git a/client/trailbase-dart/lib/src/client.dart b/client/trailbase-dart/lib/src/client.dart index 74a8227a..b7341f4e 100644 --- a/client/trailbase-dart/lib/src/client.dart +++ b/client/trailbase-dart/lib/src/client.dart @@ -362,12 +362,14 @@ class RecordApi { ); } - static Event _decodeEvent(Uint8List bytes) { + static List _decodeEvent(Uint8List bytes) { final decoded = utf8.decode(bytes); if (decoded.startsWith('data: ')) { - return Event.fromJson(jsonDecode(decoded.substring(6))); + return [Event.fromJson(jsonDecode(decoded.substring(6)))]; } - return Event.fromJson(jsonDecode(decoded)); + + // Heart-beat, do nothing. + return []; } Future> subscribe(RecordId id) async { @@ -377,7 +379,7 @@ class RecordApi { ); final Stream stream = resp.data.stream; - return stream.asyncMap(_decodeEvent); + return stream.expand(_decodeEvent); } Future> subscribeAll() async { @@ -387,7 +389,7 @@ class RecordApi { ); final Stream stream = resp.data.stream; - return stream.asyncMap(_decodeEvent); + return stream.expand(_decodeEvent); } Uri imageUri(RecordId id, String colName, {int? index}) { diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 59a5f2b1..4b191211 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -41,7 +41,7 @@ export default defineConfig({ slug: "getting-started/first-ui-app", }, { - label: "A CRUD App", + label: "A CLI App", slug: "getting-started/first-cli-app", }, { diff --git a/docs/examples/record_api_dart/.gitignore b/docs/examples/record_api_dart/.gitignore new file mode 100644 index 00000000..3a857904 --- /dev/null +++ b/docs/examples/record_api_dart/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/docs/examples/record_api_dart/analysis_options.yaml b/docs/examples/record_api_dart/analysis_options.yaml new file mode 100644 index 00000000..32774075 --- /dev/null +++ b/docs/examples/record_api_dart/analysis_options.yaml @@ -0,0 +1,18 @@ +include: package:lints/recommended.yaml + +linter: + rules: + prefer_single_quotes: true + unnecessary_brace_in_string_interps: false + unawaited_futures: true + sort_child_properties_last: false + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/docs/examples/record_api_dart/lib/record_api.dart b/docs/examples/record_api_dart/lib/record_api.dart new file mode 100644 index 00000000..e6b027ed --- /dev/null +++ b/docs/examples/record_api_dart/lib/record_api.dart @@ -0,0 +1,5 @@ +export 'src/create.dart'; +export 'src/read.dart'; +export 'src/update.dart'; +export 'src/delete.dart'; +export 'src/subscribe.dart'; diff --git a/docs/examples/record_api_dart/lib/src/create.dart b/docs/examples/record_api_dart/lib/src/create.dart new file mode 100644 index 00000000..48a9ffa4 --- /dev/null +++ b/docs/examples/record_api_dart/lib/src/create.dart @@ -0,0 +1,5 @@ +import 'package:trailbase/trailbase.dart'; + +Future create(Client client) async => await client + .records('simple_strict_table') + .create({'text_not_null': 'test'}); diff --git a/docs/examples/record_api_dart/lib/src/delete.dart b/docs/examples/record_api_dart/lib/src/delete.dart new file mode 100644 index 00000000..1adba52a --- /dev/null +++ b/docs/examples/record_api_dart/lib/src/delete.dart @@ -0,0 +1,4 @@ +import 'package:trailbase/trailbase.dart'; + +Future delete(Client client, RecordId id) async => + await client.records('simple_strict_table').delete(id); diff --git a/docs/examples/record_api_dart/lib/src/read.dart b/docs/examples/record_api_dart/lib/src/read.dart new file mode 100644 index 00000000..89dcaa87 --- /dev/null +++ b/docs/examples/record_api_dart/lib/src/read.dart @@ -0,0 +1,4 @@ +import 'package:trailbase/trailbase.dart'; + +Future> read(Client client, RecordId id) async => + await client.records('simple_strict_table').read(id); diff --git a/docs/examples/record_api_dart/lib/src/subscribe.dart b/docs/examples/record_api_dart/lib/src/subscribe.dart new file mode 100644 index 00000000..5cbe5935 --- /dev/null +++ b/docs/examples/record_api_dart/lib/src/subscribe.dart @@ -0,0 +1,9 @@ +import 'dart:async'; + +import 'package:trailbase/trailbase.dart'; + +Future> subscribe(Client client, RecordId id) async => + await client.records('simple_strict_table').subscribe(id); + +Future> subscribeAll(Client client) async => + await client.records('simple_strict_table').subscribeAll(); diff --git a/docs/examples/record_api_dart/lib/src/update.dart b/docs/examples/record_api_dart/lib/src/update.dart new file mode 100644 index 00000000..c91f4151 --- /dev/null +++ b/docs/examples/record_api_dart/lib/src/update.dart @@ -0,0 +1,5 @@ +import 'package:trailbase/trailbase.dart'; + +Future update(Client client, RecordId id) async => await client + .records('simple_strict_table') + .update(id, {'text_not_null': 'updated'}); diff --git a/docs/examples/record_api_dart/pubspec.lock b/docs/examples/record_api_dart/pubspec.lock new file mode 100644 index 00000000..a3a64fbb --- /dev/null +++ b/docs/examples/record_api_dart/pubspec.lock @@ -0,0 +1,433 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "03f6da266a27a4538a69295ec142cb5717d7d4e5727b84658b63e1e1509bac9c" + url: "https://pub.dev" + source: hosted + version: "79.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: c9040fc56483c22a5e04a9f6a251313118b1a3c42423770623128fa484115643 + url: "https://pub.dev" + source: hosted + version: "7.2.0" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + url: "https://pub.dev" + source: hosted + version: "1.11.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + dio: + dependency: transitive + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + jwt_decoder: + dependency: transitive + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: "8391fbe68d520daf2314121764d38e37f934c02fd7301ad18307bd93bd6b725d" + url: "https://pub.dev" + source: hosted + version: "1.25.14" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + test_core: + dependency: transitive + description: + name: test_core + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + url: "https://pub.dev" + source: hosted + version: "0.6.8" + trailbase: + dependency: "direct main" + description: + path: "../../../client/trailbase-dart" + relative: true + source: path + version: "0.2.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + url: "https://pub.dev" + source: hosted + version: "15.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.6.1 <4.0.0" diff --git a/docs/examples/record_api_dart/pubspec.yaml b/docs/examples/record_api_dart/pubspec.yaml new file mode 100644 index 00000000..51255693 --- /dev/null +++ b/docs/examples/record_api_dart/pubspec.yaml @@ -0,0 +1,15 @@ +name: record_api +description: Example uses of record APIs for documentation purposes. +version: 0.0.1 +publish_to: none + +environment: + sdk: ^3.6.1 + +dependencies: + trailbase: + path: ../../../client/trailbase-dart + +dev_dependencies: + lints: ^5.0.0 + test: ^1.24.0 diff --git a/docs/examples/record_api_dart/test/record_api_test.dart b/docs/examples/record_api_dart/test/record_api_test.dart new file mode 100644 index 00000000..ddd167aa --- /dev/null +++ b/docs/examples/record_api_dart/test/record_api_test.dart @@ -0,0 +1,74 @@ +import 'package:test/test.dart'; +import 'package:trailbase/trailbase.dart'; + +import 'package:record_api/record_api.dart'; + +class SimpleStrict { + final String id; + + final String? textNull; + final String textDefault; + final String textNotNull; + + SimpleStrict({ + required this.id, + this.textNull, + this.textDefault = '', + required this.textNotNull, + }); + + SimpleStrict.fromJson(Map json) + : id = json['id'], + textNull = json['text_null'], + textDefault = json['text_default'], + textNotNull = json['text_not_null']; + + Map toJson() => { + 'id': id, + 'text_null': textNull, + 'text_default': textDefault, + 'text_not_null': textNotNull, + }; +} + +Future connect() async { + final client = Client('http://localhost:4000'); + await client.login('admin@localhost', 'secret'); + return client; +} + +void main() { + test('Test examples', () async { + final client = await connect(); + + final tableStream = await subscribeAll(client); + + final id = await create(client); + + final recordStream = await subscribe(client, id); + + { + final json = await read(client, id); + final record = SimpleStrict.fromJson(json); + expect(record.textNotNull, equals('test')); + } + + { + await update(client, id); + final json = await read(client, id); + final record = SimpleStrict.fromJson(json); + expect(record.textNotNull, equals('updated')); + } + + await delete(client, id); + + expect(await recordStream.length, equals(2)); + + final tableEventList = + await tableStream.timeout(Duration(seconds: 5), onTimeout: (sink) { + print('Stream timeout'); + sink.close(); + }).toList(); + expect(tableEventList.length, equals(3)); + }); +} diff --git a/docs/src/content/docs/_roadmap.md b/docs/src/content/docs/_roadmap.md index bc64cf52..19cffa24 100644 --- a/docs/src/content/docs/_roadmap.md +++ b/docs/src/content/docs/_roadmap.md @@ -5,7 +5,6 @@ can figure out how any feature will fit into the overall picture and minimize friction. For context, some larger features we have on our Roadmap: -- Realtime/notification APIs for subscribing to data changes. - Auth: more customizable settings, more customizable UI, and multi-factor. Also, service-accounts to auth other backends as opposed to end-users. - Many SQLite databases: imagine a separate database by tenant or user. diff --git a/docs/src/content/docs/comparison/pocketbase.mdx b/docs/src/content/docs/comparison/pocketbase.mdx index 795f32ad..721bd002 100644 --- a/docs/src/content/docs/comparison/pocketbase.mdx +++ b/docs/src/content/docs/comparison/pocketbase.mdx @@ -48,7 +48,7 @@ Likewise, TrailBase has a few nifty tricks up its sleeve: - Language independent type-safety via JSON Schemas with strict typing being enforced all the way down to the database level[^4]. - TrailBase's JavaScript runtime supports full ES6, TypeScript transpilation, - and is built on V8 making it [~40x faster](/reference/benchmarks/). + and is built on V8 making it [~40x faster](/reference/benchmarks/#javascript-performance). - First-class access to all of SQLite's features and capabilities. - A simple auth UI. - Stateless JWT auth-tokens for simple, hermetic authentication in other diff --git a/docs/src/content/docs/documentation/APIs/record_apis.mdx b/docs/src/content/docs/documentation/APIs/record_apis.mdx index 245d99fc..74fb1da5 100644 --- a/docs/src/content/docs/documentation/APIs/record_apis.mdx +++ b/docs/src/content/docs/documentation/APIs/record_apis.mdx @@ -3,11 +3,12 @@ title: Record APIs --- import { Aside, Code } from "@astrojs/starlight/components"; +import { Tabs, TabItem } from '@astrojs/starlight/components'; + import { apiPath, recordApiNamePlaceholder, recordApiIdPlaceholder } from "./record_apis.ts"; -The easiest and most type-safe path to access your `TABLE`s and `VIEW`s is to use -TrailBase's restful CRUD _Record APIs_. -The only requirements are: +TrailBase's restful CRUD _Record APIs_ allow you to access your `TABLE`s and +`VIEW`s in an easy and type-safe manner. - Tables and views need to be `STRICT`ly[^1] typed to guarantee type-safety all the way from your records, via JSON schema, to your client-side language bindings [^2]. @@ -15,14 +16,14 @@ The only requirements are: and thus efficient cursor-based pagination. Either an explicit `INTEGER` or UUIDv7 `PRIMARY KEY` will do, including `FOREIGN KEY` columns. -## Configuring APIs +## Configuration -Record APIs can be configured through the admin dashboard or immediately in -TrailBase's configuration file. -Note that there are certain features that aren't yet exposed in the dashboard, -like supporting multiple APIs based on the same table or view. -In this case you can drop down to the configuration to set up as many as you -like allowing for a lot of extra flexibility around permissions and visibility. +Record APIs can be configured via the admin dashboard or in TrailBase's +configuration file. +Not all features are yet exposed via the UI like exporting the same table/view +multiple times as different API endpoints. +Editing the configuration file directly, you can set up as many as you like +allowing for some extra flexibility around permissions and visibility. An example API setup for managing user avatars: @@ -60,7 +61,7 @@ A quick explanation: match. In other words, user X cannot modify user Y's avatar. -### Access Control +### Permissions Access can be controlled through combination of a simple ACL-based system (a matrix of who and what) and custom SQL access rules of the nature: @@ -108,50 +109,87 @@ appropriately tight to avoid permission escalations. ### Write-only columns Columns with names starting with an underscore can be written on insert or -update but are hidden on reads. This is meant as a convenient convention to +updated but are hidden on reads. This is meant as a convenient convention to allow for internal data fields, e.g hiding the record owner in an otherwise public data set or hiding a user's internal credit rating from their profile. A -similar effect could otherwise be achieved by exposing a table for inserts and -updates only while proxying reads through a VIEW. +similar effect could otherwise be achieved by only allowing create/update +operations on the table and exposing a subset of columns through a readable +`VIEW`. However, this would lead to different API endpoints for read vs +create/update. -## Accessing Record APIs +## Access -After configuring the APIs and setting up permissions, record APIs expose six -main endpoints[^3]: +After setting up your API, TrailBase will expose the following main endpoints[^3]: -* **C**reate: endpoint for for inserting new and potentially overriding records - depending on conflict resolution strategy.
- POST {apiPath({name: recordApiNamePlaceholder})} -* **R**ead: endpoint for reading specific records given the record id.
- GET {apiPath({name: recordApiNamePlaceholder, suffix: recordApiIdPlaceholder})} -* **U**pdate: partial updates to existing records given a record id and subset of fields
- PATCH {apiPath({name: recordApiNamePlaceholder, suffix: recordApiIdPlaceholder})} -* **D**elete: endpoints for deleting record given a record id.
- DELETE {apiPath({name: recordApiNamePlaceholder, suffix: recordApiIdPlaceholder})} -* List: endpoint for listing, filtering and sorting records based on the - configured read access rule and provided filters.
- GET {apiPath({name: `${recordApiNamePlaceholder}?`})} -* Schema: endpoint for reading the APIs JSON schema definition. Can be used for - introspection and to drive code generation.
- GET {apiPath({name: recordApiNamePlaceholder, suffix: "schema"})} -* "realtime" subscriptions/notifications: establish a streaming HTTP connection to listen for updates via SSE: - * Specific record:
- GET {apiPath({name: recordApiNamePlaceholder, suffix: `subscribe/${recordApiIdPlaceholder}`})} - * All record:
- GET {apiPath({name: recordApiNamePlaceholder, suffix: `subscribe/*`})} +* **C**reate: POST {apiPath({name: recordApiNamePlaceholder})} +* **R**ead: GET {apiPath({name: recordApiNamePlaceholder, suffix: recordApiIdPlaceholder})} +* **U**pdate: PATCH {apiPath({name: recordApiNamePlaceholder, suffix: recordApiIdPlaceholder})} +* **D**elete: DELETE {apiPath({name: recordApiNamePlaceholder, suffix: recordApiIdPlaceholder})} +* List: GET {apiPath({name: `${recordApiNamePlaceholder}?`})} +* Change Subscriptions:
GET {apiPath({name: recordApiNamePlaceholder, suffix: `subscribe/[*|${recordApiIdPlaceholder}]`})} +* Schema: GET {apiPath({name: recordApiNamePlaceholder, suffix: "schema"})} + +All of the endpoints accept requests that are JSON encoded, url-encoded, or +`multipart/form-data` encoded, which makes them accessible via rich client-side +applications, progressive web apps, and static HTML forms alike. + +### Create + +The create endpoint lets you insert new records and potentially override +existing ones depending on conflict resolution strategy. + +import createDartCode from "@examples/record_api_dart/lib/src/create.dart?raw"; + + + + + + -All of the above endpoints can be interacted with through requests that are -either JSON encoded, url-encoded, or `multipart/form-data` encoded, which makes -them accessible via rich client-side applications, progressive web apps, and -static HTML forms alike. +### Read -### Listing, filtering & sorting records +The read endpoint lets you read specific records given their id. + +import readDartCode from "@examples/record_api_dart/lib/src/read.dart?raw"; + + + + + + + +### Update + +The update endpoint lets you modify, i.e. partially update, existing records given their id + +import updateDartCode from "@examples/record_api_dart/lib/src/update.dart?raw"; + + + + + + + +### Delete + +import deleteDartCode from "@examples/record_api_dart/lib/src/update.dart?raw"; + + + + + + + +The delete endpoints lets you remove a record given its id. + + +### List: Filter, Sort and Paginate Using the GET {apiPath({name: `${recordApiNamePlaceholder}?`})} endpoint and given sufficient permissions one can query records based the given `read_access_rule` @@ -185,7 +223,27 @@ export const curlListRecords = -## File Upload +### Subscribe + +The streaming subscribe endpoints lets you listen for changes to tables backing +an API or specific records given their id. Change events can be insertions, +updates, and deletions. + +import subscribeDartCode from "@examples/record_api_dart/lib/src/subscribe.dart?raw"; + + + + + + + +### Schema + +The schema endpoint allows for reading the APIs JSON schema definition. This +can be useful for introspection such as driving external code generation. + + +## File Uploads Record APIs can also support file uploads and downloads. There's some special handling in place so that only metadata is stored in the underlying table while @@ -254,7 +312,9 @@ When generating new client-side bindings for a table or view with such nested schemas, they will be included ensuring type-safety all the way to the client-side APIs. -### Tangent: Querying JSON +{/* + +## Tangent: Querying JSON Independent of type-safety and Record APIs, [SQLite](https://www.sqlite.org/json1.html) has first-class support for @@ -286,6 +346,7 @@ on it would be a lot more efficient. Yet, using JSON for complex structured or denormalized data can be powerful addition to your toolbox. +*/}
@@ -305,8 +366,8 @@ addition to your toolbox. that you think should be supported but aren't. [^3]: - There's also a few other endpoints, e.g. for downloading files as described - later in the document. + There's also a few other endpoints, e.g. for downloading files, which will + be described further down in the docs. [^4]: Record APIs only support textual JSON. Binary JSON is more compact and more diff --git a/docs/src/content/docs/documentation/APIs/record_apis.ts b/docs/src/content/docs/documentation/APIs/record_apis.ts index 4a3bbffe..c96310be 100644 --- a/docs/src/content/docs/documentation/APIs/record_apis.ts +++ b/docs/src/content/docs/documentation/APIs/record_apis.ts @@ -4,7 +4,7 @@ type ApiOptions = { prefix?: string; }; -export const recordApiNamePlaceholder = ""; +export const recordApiNamePlaceholder = ""; export const recordApiIdPlaceholder = ""; export function apiPath(opts: ApiOptions): string { diff --git a/docs/src/content/docs/getting-started/first-cli-app.mdx b/docs/src/content/docs/getting-started/first-cli-app.mdx index 2a5ff3d3..640f997e 100644 --- a/docs/src/content/docs/getting-started/first-cli-app.mdx +++ b/docs/src/content/docs/getting-started/first-cli-app.mdx @@ -1,5 +1,5 @@ --- -title: A CRUD App +title: A CLI App --- import { Code } from "@astrojs/starlight/components"; @@ -161,7 +161,7 @@ version and copy&paste the JSON schema from above. With the generated types, we can use the TrailBase TypeScript client to write the following program: -import fillCode from "../../../../../examples/tutorial/scripts/src/fill.ts?raw"; +import fillCode from "@root/examples/tutorial/scripts/src/fill.ts?raw"; diff --git a/docs/src/content/docs/reference/benchmarks.mdx b/docs/src/content/docs/reference/benchmarks.mdx index d627d8e9..c0922915 100644 --- a/docs/src/content/docs/reference/benchmarks.mdx +++ b/docs/src/content/docs/reference/benchmarks.mdx @@ -179,7 +179,7 @@ being roughly 5 times slower than p50. Slower insertions can take north of 100ms. This may be related to GC pauses, scheduling, or more generally the same CPU variability we observed earlier. -## JavaScript-Runtime Benchmarks +## JavaScript Performance The benchmark sets up a custom HTTP endpoint `/fibonacci?n=` using the same slow recursive Fibonacci diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 434ac3fe..b8cf914b 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -6,7 +6,9 @@ "baseUrl": "./", "paths": { "@/*": ["./src/*"], - "@assets/*": ["../assets/*"] + "@assets/*": ["../assets/*"], + "@examples/*": ["./examples/*"], + "@root/*": ["../*"] } }, "exclude": [