render-workflows-dart 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/LICENSE +21 -0
  3. package/README.md +678 -0
  4. package/dart/generator/bin/generate.dart +419 -0
  5. package/dart/generator/pubspec.lock +149 -0
  6. package/dart/generator/pubspec.yaml +9 -0
  7. package/examples/README.md +56 -0
  8. package/examples/default/README.md +18 -0
  9. package/examples/default/gitignore +7 -0
  10. package/examples/default/index.js +2 -0
  11. package/examples/default/package.json +17 -0
  12. package/examples/default/pubspec.yaml +7 -0
  13. package/examples/default/tasks.dart +39 -0
  14. package/examples/http/README.md +26 -0
  15. package/examples/http/gitignore +7 -0
  16. package/examples/http/index.js +2 -0
  17. package/examples/http/package.json +18 -0
  18. package/examples/http/pubspec.yaml +13 -0
  19. package/examples/http/tasks.dart +100 -0
  20. package/examples/introspect/README.md +50 -0
  21. package/examples/introspect/gitignore +7 -0
  22. package/examples/introspect/index.js +2 -0
  23. package/examples/introspect/node_env.dart +35 -0
  24. package/examples/introspect/package.json +18 -0
  25. package/examples/introspect/pubspec.yaml +23 -0
  26. package/examples/introspect/tasks.dart +144 -0
  27. package/examples/native/README.md +32 -0
  28. package/examples/native/gitignore +7 -0
  29. package/examples/native/index.js +2 -0
  30. package/examples/native/native/tools_impl.dart +105 -0
  31. package/examples/native/package.json +23 -0
  32. package/examples/native/pubspec.yaml +7 -0
  33. package/examples/native/tasks.dart +35 -0
  34. package/examples/postgres/README.md +73 -0
  35. package/examples/postgres/gitignore +7 -0
  36. package/examples/postgres/index.js +2 -0
  37. package/examples/postgres/native/db_impl.dart +205 -0
  38. package/examples/postgres/package.json +26 -0
  39. package/examples/postgres/pubspec.yaml +14 -0
  40. package/examples/postgres/seed/bin/seed.dart +56 -0
  41. package/examples/postgres/seed/bin/show.dart +64 -0
  42. package/examples/postgres/seed/lib/src/connect.dart +93 -0
  43. package/examples/postgres/seed/lib/src/schema.dart +46 -0
  44. package/examples/postgres/seed/pubspec.yaml +17 -0
  45. package/examples/postgres/tasks.dart +66 -0
  46. package/package.json +51 -0
  47. package/runtime/AGENTS.md +141 -0
  48. package/runtime/CLAUDE.md +2 -0
  49. package/runtime/native_task.dart +115 -0
  50. package/runtime/render_dart.dart +428 -0
  51. package/src/cli.js +396 -0
  52. package/src/native-worker.js +196 -0
  53. package/src/node-bridge.js +118 -0
  54. package/src/runtime.js +108 -0
  55. package/src/toolchain/compile.js +153 -0
  56. package/src/toolchain/dart-sdk.js +216 -0
  57. package/src/toolchain/dart-version.js +113 -0
  58. package/src/toolchain/generate.js +112 -0
  59. package/src/toolchain/index.js +8 -0
  60. package/src/toolchain/native.js +217 -0
  61. package/src/web-shims.js +281 -0
@@ -0,0 +1,7 @@
1
+ name: render_dart_native_example
2
+ description: Reaching dart:io, dart:ffi and isolates from a Render Workflows task.
3
+ publish_to: none
4
+ version: 0.1.0
5
+
6
+ environment:
7
+ sdk: ^3.12.0
@@ -0,0 +1,35 @@
1
+ import 'render_dart.dart';
2
+ // The plain name. Nothing here says these run in another process.
3
+ import 'native/tools.dart';
4
+
5
+ /// Tasks backed by natively compiled Dart.
6
+ ///
7
+ /// `render-dart build` compiles `native/tools_impl.dart` to an executable and
8
+ /// generates `native/tools.dart`, which resolves to a process call under
9
+ /// dart2js and to the real function when compiled natively.
10
+ ///
11
+ /// Always `await` one: the stub returns a `Future` where the implementation
12
+ /// may return a plain value.
13
+ void main() {
14
+ /// Reading a file — impossible from dart2js.
15
+ task('readFile', (args) async => await readFile(
16
+ args[0]! as String,
17
+ previewLines: args.length < 2 ? 3 : args[1]! as int,
18
+ ));
19
+
20
+ /// Shelling out.
21
+ task('runCommand', (args) async => await runCommand(
22
+ args[0]! as String,
23
+ (args.length < 2 ? const <Object?>[] : args[1]! as List<Object?>).cast<String>(),
24
+ ));
25
+
26
+ /// dart:ffi and platform details.
27
+ task('platformInfo', (args) async => await platformInfo());
28
+
29
+ /// Real parallelism across cores.
30
+ task('parallelHash', (args) async => await parallelHash(
31
+ (args[0]! as List<Object?>).cast<String>(),
32
+ ), timeoutSeconds: 120);
33
+
34
+ start();
35
+ }
@@ -0,0 +1,73 @@
1
+ # postgres
2
+
3
+ Reading and writing a Render Postgres database from a Dart workflow task.
4
+
5
+ This is the clearest case for native tasks existing. `package:postgres` speaks
6
+ the wire protocol over a raw socket, so it needs `dart:io` — pub.dev marks it
7
+ `runtime:native-aot` and `runtime:native-jit`, with **no** `runtime:web`. It
8
+ cannot run under dart2js at all, and there is no WebAssembly build to fall back
9
+ on. Native or nothing.
10
+
11
+ ## The two halves
12
+
13
+ `seed/` runs on **your machine**, over the database's *external* connection
14
+ string. It creates the table and fills it.
15
+
16
+ The tasks run **on Render**, over the *internal* one. So when a task changes a
17
+ row and `seed/bin/show.dart` sees it, the data has crossed two machines and two
18
+ connections — which is the thing worth demonstrating.
19
+
20
+ | Task | Shows |
21
+ | --- | --- |
22
+ | `listWidgets` | A parameterised query |
23
+ | `widgetStats` | An aggregate |
24
+ | `upsertWidget` | Insert or update |
25
+ | `restock` | `update … returning`, in one statement so concurrent runs cannot lose an update |
26
+ | `dbConnection` | `pg_backend_pid()` — proof the connection is held |
27
+
28
+ ## Running it
29
+
30
+ ```bash
31
+ cd seed
32
+ export RENDER_API_KEY=rnd_... # the connection string is fetched for you
33
+ dart pub get && dart run bin/seed.dart
34
+
35
+ cd ..
36
+ export DATABASE_URL=<external connection string>
37
+ npm install && npx render-dart dev
38
+ render workflows start widgetStats --local --input='[]'
39
+ ```
40
+
41
+ `seed.dart` is re-runnable: `create table if not exists`, rows upserted by sku.
42
+ That matters because a **free** Render Postgres instance is deleted 30 days
43
+ after creation, so this is the recipe for rebuilding the demo.
44
+
45
+ ## Deploying
46
+
47
+ `DATABASE_URL` must hold the database's **internal** connection string. A
48
+ workflow's environment can only be set when the service is created — pass it
49
+ then, because `PATCH /workflows/{id}` cannot add one afterwards, leaving only
50
+ the Dashboard.
51
+
52
+ ## Three things that will bite
53
+
54
+ **Worker mode is doing real work here.** Every task declares
55
+ `@NativeTask(worker: true)`, so the process stays alive and the TCP handshake,
56
+ TLS negotiation and authentication happen once rather than per call.
57
+ `dbConnection` proves it: the backend pid holds steady while a counter climbs.
58
+
59
+ **A held connection can die** — idle timeout, a restart, a deploy. `_db()`
60
+ checks and reconnects rather than surfacing a broken socket. That is the honest
61
+ cost of keeping state in a worker.
62
+
63
+ **`timestamptz` arrives as a `DateTime`, which is not JSON.** Rows are
64
+ converted to ISO-8601 strings before returning; otherwise the build rejects the
65
+ signature — correct, but puzzling if unexpected.
66
+
67
+ ## A note on the connection string
68
+
69
+ Render's *internal* string carries no port, and `Uri.port` reports `0` for
70
+ that, which is not a port. `seed/lib/src/connect.dart` applies 5432 explicitly.
71
+ `Connection.openFromUrl` exists but takes TLS from an `sslmode` query parameter
72
+ Render's strings do not carry, so the endpoint is built by hand with
73
+ `SslMode.require`.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ build/
3
+ .dart_tool/
4
+ .dart-sdk/
5
+ pubspec.lock
6
+ .env
7
+ .DS_Store
@@ -0,0 +1,2 @@
1
+ // Render runs this as the start command.
2
+ require('render-dart/runtime').runTasks('./build/tasks.js');
@@ -0,0 +1,205 @@
1
+ // Database access for the workflow tasks, compiled AOT.
2
+ //
3
+ // This is the case where native is not a convenience but the only option:
4
+ // package:postgres speaks the wire protocol over a raw socket, so it needs
5
+ // dart:io. pub.dev marks it runtime:native-aot and runtime:native-jit, with no
6
+ // runtime:web — under dart2js it cannot run at all.
7
+ //
8
+ // Every task here declares worker: true. That keeps one process alive, and
9
+ // with it one open connection, so the TCP handshake, TLS negotiation and
10
+ // Postgres authentication happen once instead of on every call.
11
+ import 'dart:io';
12
+
13
+ import 'package:postgres/postgres.dart';
14
+
15
+ import '../native_task.dart';
16
+
17
+ const _worker = NativeTask(
18
+ worker: true,
19
+ idleTimeout: Duration(minutes: 2),
20
+ timeout: Duration(seconds: 30),
21
+ );
22
+
23
+ Connection? _connection;
24
+ int _calls = 0;
25
+ DateTime? _connectedAt;
26
+
27
+ /// The held connection, opened on first use and re-established if it died.
28
+ ///
29
+ /// A worker's state surviving between calls is what makes it fast, and also
30
+ /// what makes this necessary: Postgres can hang up on an idle client, a deploy
31
+ /// restarts the database, and the socket is then dead in a way that only shows
32
+ /// up on the next query. Checking beats surfacing a broken pipe to the caller.
33
+ Future<Connection> _db() async {
34
+ final existing = _connection;
35
+ if (existing != null && existing.isOpen) return existing;
36
+
37
+ if (existing != null) {
38
+ stderr.writeln('[db] connection was closed, reconnecting');
39
+ _connection = null;
40
+ }
41
+
42
+ final url = Platform.environment['DATABASE_URL'];
43
+ if (url == null || url.isEmpty) {
44
+ throw StateError(
45
+ 'DATABASE_URL is not set on this service. It should hold the database\'s '
46
+ 'internal connection string — set it when creating the workflow, or in '
47
+ 'the Dashboard under the service\'s Environment.',
48
+ );
49
+ }
50
+
51
+ final uri = Uri.parse(url);
52
+ final userInfo = uri.userInfo.split(':');
53
+
54
+ final connection = await Connection.open(
55
+ Endpoint(
56
+ host: uri.host,
57
+ // Render's internal connection string carries no port, and Uri reports 0
58
+ // rather than null for that, which is not a port.
59
+ port: uri.hasPort ? uri.port : 5432,
60
+ database: uri.pathSegments.isEmpty ? 'postgres' : uri.pathSegments.first,
61
+ username: userInfo.isNotEmpty ? Uri.decodeComponent(userInfo.first) : null,
62
+ password: userInfo.length > 1 ? Uri.decodeComponent(userInfo[1]) : null,
63
+ ),
64
+ settings: const ConnectionSettings(
65
+ sslMode: SslMode.require,
66
+ applicationName: 'render-dart-db-test',
67
+ connectTimeout: Duration(seconds: 15),
68
+ ),
69
+ );
70
+
71
+ _connection = connection;
72
+ _connectedAt = DateTime.now();
73
+ return connection;
74
+ }
75
+
76
+ /// Rows come back with DateTime values, which are not JSON. Converting here
77
+ /// keeps every task's return type inside what can cross the boundary —
78
+ /// otherwise `render-dart build` rejects the signature, correctly but
79
+ /// confusingly if you were not expecting it.
80
+ Map<String, Object?> _jsonRow(ResultRow row) => {
81
+ for (final entry in row.toColumnMap().entries)
82
+ entry.key: switch (entry.value) {
83
+ final DateTime v => v.toIso8601String(),
84
+ final v => v,
85
+ },
86
+ };
87
+
88
+ /// Lists widgets, newest update first, optionally filtered by sku.
89
+ @_worker
90
+ Future<List<Map<String, Object?>>> listWidgets({String? sku, int limit = 20}) async {
91
+ _calls++;
92
+ final db = await _db();
93
+
94
+ final result = sku == null
95
+ ? await db.execute(
96
+ Sql.named('select * from widgets order by sku limit @limit'),
97
+ parameters: {'limit': limit},
98
+ )
99
+ : await db.execute(
100
+ Sql.named('select * from widgets where sku = @sku'),
101
+ parameters: {'sku': sku},
102
+ );
103
+
104
+ return result.map(_jsonRow).toList();
105
+ }
106
+
107
+ /// A single aggregate row: how much stock there is and what it is worth.
108
+ @_worker
109
+ Future<Map<String, Object?>> widgetStats() async {
110
+ _calls++;
111
+ final db = await _db();
112
+
113
+ final result = await db.execute('''
114
+ select count(*) as widgets,
115
+ coalesce(sum(quantity), 0) as units,
116
+ coalesce(sum(quantity * price_cents), 0) as value_cents,
117
+ max(updated_at) as newest_update
118
+ from widgets
119
+ ''');
120
+
121
+ return _jsonRow(result.first);
122
+ }
123
+
124
+ /// Inserts a widget, or updates it if the sku already exists.
125
+ @_worker
126
+ Future<Map<String, Object?>> upsertWidget(
127
+ String sku,
128
+ String name,
129
+ int quantity,
130
+ int priceCents,
131
+ ) async {
132
+ _calls++;
133
+ final db = await _db();
134
+
135
+ final result = await db.execute(
136
+ Sql.named('''
137
+ insert into widgets (sku, name, quantity, price_cents)
138
+ values (@sku, @name, @quantity, @priceCents)
139
+ on conflict (sku) do update set
140
+ name = excluded.name,
141
+ quantity = excluded.quantity,
142
+ price_cents = excluded.price_cents,
143
+ updated_at = now()
144
+ returning *
145
+ '''),
146
+ parameters: {
147
+ 'sku': sku,
148
+ 'name': name,
149
+ 'quantity': quantity,
150
+ 'priceCents': priceCents,
151
+ },
152
+ );
153
+
154
+ return _jsonRow(result.first);
155
+ }
156
+
157
+ /// Adds [delta] to a widget's quantity and returns the row that changed.
158
+ ///
159
+ /// Done in one statement rather than read-then-write, so two concurrent runs
160
+ /// cannot lose an update between them.
161
+ @_worker
162
+ Future<Map<String, Object?>> restock(String sku, int delta) async {
163
+ _calls++;
164
+ final db = await _db();
165
+
166
+ final result = await db.execute(
167
+ Sql.named('''
168
+ update widgets
169
+ set quantity = quantity + @delta,
170
+ updated_at = now()
171
+ where sku = @sku
172
+ returning *
173
+ '''),
174
+ parameters: {'sku': sku, 'delta': delta},
175
+ );
176
+
177
+ if (result.isEmpty) throw ArgumentError.value(sku, 'sku', 'no widget with that sku');
178
+ return _jsonRow(result.first);
179
+ }
180
+
181
+ /// Proves the connection is being reused, from the server's point of view.
182
+ ///
183
+ /// `pg_backend_pid()` is the Postgres process serving this session. If it holds
184
+ /// steady across calls, the worker really is reusing one connection rather than
185
+ /// reconnecting — the database-side counterpart to the process pid in
186
+ /// nativeLoop.
187
+ @_worker
188
+ Future<Map<String, Object?>> dbConnection() async {
189
+ _calls++;
190
+ final db = await _db();
191
+
192
+ final result = await db.execute(
193
+ 'select pg_backend_pid() as backend_pid, current_database() as database, '
194
+ 'version() as server',
195
+ );
196
+ final row = _jsonRow(result.first);
197
+
198
+ return {
199
+ ...row,
200
+ 'processPid': pid,
201
+ 'callsThisProcess': _calls,
202
+ 'connectionAgeMs':
203
+ _connectedAt == null ? 0 : DateTime.now().difference(_connectedAt!).inMilliseconds,
204
+ };
205
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "render-dart-postgres-example",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Reading and writing Render Postgres from a Dart workflow task",
6
+ "scripts": {
7
+ "build": "render-dart build",
8
+ "start": "node index.js",
9
+ "dev": "render-dart dev"
10
+ },
11
+ "dependencies": {
12
+ "@renderinc/sdk": "^0.6.0",
13
+ "render-dart": "^0.7.1"
14
+ },
15
+ "renderDart": {
16
+ "native": [
17
+ "native/db_impl.dart"
18
+ ],
19
+ "allowDartIoIn": [
20
+ "seed"
21
+ ]
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ name: render_dart_postgres_example
2
+ description: Reading and writing Render Postgres from a Dart workflow task.
3
+ publish_to: none
4
+ version: 0.1.0
5
+
6
+ environment:
7
+ sdk: ^3.12.0
8
+
9
+ dependencies:
10
+ # Speaks the Postgres wire protocol over a raw socket, so it needs dart:io.
11
+ # pub.dev marks it runtime:native-aot and native-jit, with no runtime:web —
12
+ # under dart2js it cannot run at all. That is why the tasks that use it are
13
+ # native, and it is the clearest case for native tasks existing.
14
+ postgres: ^3.5.12
@@ -0,0 +1,56 @@
1
+ // Creates the widgets table and fills it with sample rows.
2
+ //
3
+ // dart run bin/seed.dart
4
+ // dart run bin/seed.dart --database-id dpg-...
5
+ // DATABASE_URL=postgresql://... dart run bin/seed.dart
6
+ //
7
+ // Safe to re-run: the table is created only if absent, and rows are upserted
8
+ // by sku. That matters because a free Render Postgres instance is deleted 30
9
+ // days after creation, so this is the recipe for getting the demo back.
10
+ //
11
+ // This is deliberately NOT a workflow task. It runs from a laptop over the
12
+ // external connection string, which is the other half of the demonstration —
13
+ // the tasks later read the same rows from inside Render.
14
+ import 'dart:io';
15
+
16
+ import 'package:render_dart_postgres_seed/src/connect.dart';
17
+ import 'package:render_dart_postgres_seed/src/schema.dart';
18
+
19
+ Future<void> main(List<String> args) async {
20
+ final databaseId = _option(args, 'database-id') ?? defaultDatabaseId;
21
+
22
+ final connectionString = await resolveConnectionString(databaseId: databaseId);
23
+ stdout.writeln('connecting to ${describe(connectionString)}');
24
+
25
+ final db = await openConnection(connectionString);
26
+ try {
27
+ await db.execute(createWidgetsTable);
28
+ stdout.writeln('table widgets is ready');
29
+
30
+ for (final widget in dummyWidgets) {
31
+ final result = await db.execute(
32
+ Sql.named(upsertWidget),
33
+ parameters: widget,
34
+ );
35
+ final row = result.first.toColumnMap();
36
+ stdout.writeln(' ${row['sku']} ${row['name']}'
37
+ ' qty ${row['quantity']}'
38
+ ' ${_money(row['price_cents'] as int)}');
39
+ }
40
+
41
+ final count = await db.execute('select count(*) from widgets');
42
+ stdout.writeln('\n${count.first.first} row(s) in widgets');
43
+ } finally {
44
+ await db.close();
45
+ }
46
+ }
47
+
48
+ String _money(int cents) => '\$${(cents / 100).toStringAsFixed(2)}';
49
+
50
+ String? _option(List<String> args, String name) {
51
+ for (var i = 0; i < args.length; i++) {
52
+ if (args[i] == '--$name' && i + 1 < args.length) return args[i + 1];
53
+ if (args[i].startsWith('--$name=')) return args[i].substring(name.length + 3);
54
+ }
55
+ return null;
56
+ }
@@ -0,0 +1,64 @@
1
+ // Prints what is actually in the widgets table.
2
+ //
3
+ // dart run bin/show.dart
4
+ // dart run bin/show.dart --sku WDG-005
5
+ //
6
+ // Useful as the independent check on a workflow task: run a task that mutates
7
+ // a row, then run this from your laptop and see the change. Different machine,
8
+ // different connection string, same data.
9
+ import 'dart:io';
10
+
11
+ import 'package:render_dart_postgres_seed/src/connect.dart';
12
+
13
+ Future<void> main(List<String> args) async {
14
+ final sku = _option(args, 'sku');
15
+
16
+ final connectionString = await resolveConnectionString();
17
+ final db = await openConnection(connectionString);
18
+
19
+ try {
20
+ final result = sku == null
21
+ ? await db.execute(
22
+ 'select sku, name, quantity, price_cents, updated_at '
23
+ 'from widgets order by sku')
24
+ : await db.execute(
25
+ Sql.named('select sku, name, quantity, price_cents, updated_at '
26
+ 'from widgets where sku = @sku'),
27
+ parameters: {'sku': sku},
28
+ );
29
+
30
+ if (result.isEmpty) {
31
+ stdout.writeln(sku == null ? 'widgets is empty' : 'no widget with sku $sku');
32
+ return;
33
+ }
34
+
35
+ stdout.writeln('${'sku'.padRight(10)} ${'name'.padRight(24)} '
36
+ '${'qty'.padLeft(6)} ${'price'.padLeft(9)} updated');
37
+ var totalCents = 0;
38
+ for (final row in result) {
39
+ final r = row.toColumnMap();
40
+ final quantity = r['quantity'] as int;
41
+ final priceCents = r['price_cents'] as int;
42
+ totalCents += quantity * priceCents;
43
+
44
+ stdout.writeln('${(r['sku'] as String).padRight(10)} '
45
+ '${(r['name'] as String).padRight(24)} '
46
+ '${quantity.toString().padLeft(6)} '
47
+ '${_money(priceCents).padLeft(9)} '
48
+ '${(r['updated_at'] as DateTime).toIso8601String().substring(0, 19)}');
49
+ }
50
+ stdout.writeln('\n${result.length} row(s), inventory value ${_money(totalCents)}');
51
+ } finally {
52
+ await db.close();
53
+ }
54
+ }
55
+
56
+ String _money(int cents) => '\$${(cents / 100).toStringAsFixed(2)}';
57
+
58
+ String? _option(List<String> args, String name) {
59
+ for (var i = 0; i < args.length; i++) {
60
+ if (args[i] == '--$name' && i + 1 < args.length) return args[i + 1];
61
+ if (args[i].startsWith('--$name=')) return args[i].substring(name.length + 3);
62
+ }
63
+ return null;
64
+ }
@@ -0,0 +1,93 @@
1
+ /// Getting a connection to the Render Postgres instance.
2
+ ///
3
+ /// Two credential sources, in order:
4
+ ///
5
+ /// DATABASE_URL if set, used as-is. Works offline, against a local
6
+ /// Postgres, or anywhere an API key is unavailable.
7
+ /// RENDER_API_KEY otherwise, the connection string is fetched through
8
+ /// package:render_api. Nothing to copy, nothing to keep in
9
+ /// a file, and it survives the database being recreated.
10
+ library;
11
+
12
+ import 'dart:io';
13
+
14
+ import 'package:postgres/postgres.dart';
15
+ import 'package:render_api/render_api.dart';
16
+
17
+ // Callers need Sql and Connection alongside these helpers; re-exporting
18
+ // keeps them to a single import.
19
+ export 'package:postgres/postgres.dart' show Connection, Endpoint, Sql, SslMode;
20
+
21
+ /// The example's database. An id is an identifier, not a secret.
22
+ const defaultDatabaseId = 'dpg-da3b13gae00c73ag6t8g-a';
23
+
24
+ /// Resolves a connection string without opening anything.
25
+ ///
26
+ /// [internal] picks Render's private hostname, which only resolves from inside
27
+ /// Render's network and in the same region. Local programs want the external
28
+ /// one; a workflow task wants internal — faster, and it never leaves Render.
29
+ Future<String> resolveConnectionString({
30
+ String databaseId = defaultDatabaseId,
31
+ bool internal = false,
32
+ }) async {
33
+ final fromEnv = Platform.environment['DATABASE_URL'];
34
+ if (fromEnv != null && fromEnv.isNotEmpty) return fromEnv;
35
+
36
+ if ((Platform.environment['RENDER_API_KEY'] ?? '').isEmpty) {
37
+ throw StateError(
38
+ 'Neither DATABASE_URL nor RENDER_API_KEY is set.\n'
39
+ ' export RENDER_API_KEY=rnd_... and the connection string is fetched for you\n'
40
+ ' export DATABASE_URL=postgresql://... to point somewhere else entirely',
41
+ );
42
+ }
43
+
44
+ final render = RenderApi();
45
+ try {
46
+ final info = await render.retrievePostgresConnectionInfo(postgresId: databaseId);
47
+ return internal ? info.internalConnectionString : info.externalConnectionString;
48
+ } finally {
49
+ render.close();
50
+ }
51
+ }
52
+
53
+ /// Turns a connection string into an [Endpoint].
54
+ ///
55
+ /// `Connection.openFromUrl` exists, but it takes TLS from an `sslmode` query
56
+ /// parameter that Render's strings do not carry, and Render's *internal*
57
+ /// string has no port at all — `Uri.port` reports 0 for that, which is not a
58
+ /// port. Both are handled here rather than left to chance.
59
+ Endpoint endpointFor(String connectionString) {
60
+ final uri = Uri.parse(connectionString);
61
+ final userInfo = uri.userInfo.split(':');
62
+
63
+ return Endpoint(
64
+ host: uri.host,
65
+ port: uri.hasPort ? uri.port : 5432,
66
+ database: uri.pathSegments.isEmpty ? 'postgres' : uri.pathSegments.first,
67
+ username: userInfo.isNotEmpty ? Uri.decodeComponent(userInfo.first) : null,
68
+ password: userInfo.length > 1 ? Uri.decodeComponent(userInfo[1]) : null,
69
+ );
70
+ }
71
+
72
+ /// Opens a connection, with TLS.
73
+ ///
74
+ /// Render requires TLS for external connections and supports it internally.
75
+ /// [SslMode.require] encrypts without verifying the certificate chain, which
76
+ /// is what Render's managed certificates need; [SslMode.verifyFull] would want
77
+ /// a `securityContext` carrying their CA.
78
+ Future<Connection> openConnection(String connectionString) => Connection.open(
79
+ endpointFor(connectionString),
80
+ settings: const ConnectionSettings(
81
+ sslMode: SslMode.require,
82
+ applicationName: 'render_postgres_example',
83
+ connectTimeout: Duration(seconds: 15),
84
+ ),
85
+ );
86
+
87
+ /// Hides the password in a connection string, for printing.
88
+ String describe(String connectionString) {
89
+ final uri = Uri.parse(connectionString);
90
+ final user = uri.userInfo.split(':').first;
91
+ final port = uri.hasPort ? uri.port : 5432;
92
+ return '$user@${uri.host}:$port${uri.path}';
93
+ }
@@ -0,0 +1,46 @@
1
+ /// The table this example uses, and the rows it starts with.
2
+ ///
3
+ /// Kept in one place deliberately: the workflow tasks only ever read and write
4
+ /// this table, they never define it. One definition means the seeder is the
5
+ /// single answer to "what shape is the data".
6
+ library;
7
+
8
+ /// Creating the table is idempotent, so seeding can be re-run at will —
9
+ /// which matters because a free Render Postgres instance is deleted 30 days
10
+ /// after it is created, and this is what makes it reproducible.
11
+ const createWidgetsTable = '''
12
+ create table if not exists widgets (
13
+ id serial primary key,
14
+ sku text not null unique,
15
+ name text not null,
16
+ quantity integer not null default 0,
17
+ price_cents integer not null,
18
+ updated_at timestamptz not null default now()
19
+ )
20
+ ''';
21
+
22
+ /// Inserting is idempotent too: a re-run refreshes a row rather than failing
23
+ /// on the unique sku, or quietly doubling the inventory.
24
+ const upsertWidget = '''
25
+ insert into widgets (sku, name, quantity, price_cents)
26
+ values (@sku, @name, @quantity, @priceCents)
27
+ on conflict (sku) do update set
28
+ name = excluded.name,
29
+ quantity = excluded.quantity,
30
+ price_cents = excluded.price_cents,
31
+ updated_at = now()
32
+ returning id, sku, name, quantity, price_cents, updated_at
33
+ ''';
34
+
35
+ /// A small, boring inventory. Enough rows to make a `where` and an aggregate
36
+ /// mean something, few enough to read in a terminal.
37
+ const dummyWidgets = <Map<String, Object?>>[
38
+ {'sku': 'WDG-001', 'name': 'Hex bolt, M8', 'quantity': 500, 'priceCents': 12},
39
+ {'sku': 'WDG-002', 'name': 'Hex nut, M8', 'quantity': 480, 'priceCents': 7},
40
+ {'sku': 'WDG-003', 'name': 'Flat washer, M8', 'quantity': 1200, 'priceCents': 3},
41
+ {'sku': 'WDG-004', 'name': 'Socket cap screw, M6', 'quantity': 220, 'priceCents': 21},
42
+ {'sku': 'WDG-005', 'name': 'Ball bearing, 608', 'quantity': 64, 'priceCents': 145},
43
+ {'sku': 'WDG-006', 'name': 'Timing belt, 200mm', 'quantity': 18, 'priceCents': 890},
44
+ {'sku': 'WDG-007', 'name': 'Linear rail, 300mm', 'quantity': 6, 'priceCents': 2450},
45
+ {'sku': 'WDG-008', 'name': 'Stepper motor, NEMA 17', 'quantity': 12, 'priceCents': 1799},
46
+ ];
@@ -0,0 +1,17 @@
1
+ name: render_dart_postgres_seed
2
+ description: Creates and fills the table the workflow tasks read.
3
+ publish_to: none
4
+ version: 0.1.0
5
+
6
+ environment:
7
+ sdk: ^3.9.0
8
+
9
+ dependencies:
10
+ postgres: ^3.5.12
11
+ # Fetches the connection string, so nothing secret lives in a file.
12
+ #
13
+ # A git dependency while render_api is unpublished; becomes a version
14
+ # constraint once it is on pub.dev.
15
+ render_api:
16
+ git:
17
+ url: https://github.com/timmaffett/render_api_sdk.git