An open API service indexing awesome lists of open source software.

https://github.com/moinsen-dev/supabase_client_gen


https://github.com/moinsen-dev/supabase_client_gen

Last synced: 18 days ago
JSON representation

Awesome Lists containing this project

README

          

# supabase_client_gen

Contract-driven code generator for Supabase. Define your backend in a single
YAML file, then generate a complete typed Dart client — models, enums,
repositories, edge function clients, and storage bucket clients.

```bash
# Install globally:
dart pub global activate supabase_client_gen

# Generate from any project:
dart pub global run supabase_client_gen:generate \
--contract docs/contracts/supabase.yaml \
--output lib/generated
```

## Why?

Writing Supabase client code by hand means repeating yourself across tables,
keeping Dart types in sync with Postgres columns, and wiring up realtime
subscriptions manually. This tool reads a single contract file — your backend's
source of truth — and produces all the Dart code you need.

**One contract. One command. Zero drift.**

The same contract always produces byte-identical output (it does not depend on
whether a database is reachable), so generated code is safe to commit and gate
in CI.

## Installation

```bash
dart pub global activate supabase_client_gen
```

Or add as a dev dependency:

```yaml
dev_dependencies:
supabase_client_gen: ^0.2.0
```

### Runtime dependencies in the consuming app

Generated code imports the following — add them to the app that consumes the
generated client:

```yaml
dependencies:
supabase_flutter: ^2.8.0 # repositories, edge functions, storage
equatable: ^2.0.0 # models
```

## What Gets Generated

| Output | Path | Description |
|---|---|---|
| Data models | `models/` | `Equatable` classes with `fromJson`/`toJson`/`copyWith`, null-safe |
| Enums | `enums/enums.dart` | Dart enums from Postgres enum types, with `fromString` |
| Repositories | `repositories/` | Typed `select`/`insert`/`update`/`delete`/`stream` |
| Edge function clients | `edge_functions/edge_functions.dart` | Typed wrappers over `functions.invoke` |
| Storage clients | `storage/storage.dart` | Per-bucket upload/download/URL with MIME + size checks |

Every generated file carries a `// GENERATED by supabase_client_gen` header and
must not be hand-edited.

### Nullability

Nullability comes **only** from the contract's `nullable_fields` lists — this is
what keeps generated output deterministic and machine-independent.

To keep those lists honest against the real database, let the DB write them back
into the contract:

```bash
dart pub global run supabase_client_gen:generate \
--contract docs/contracts/supabase.yaml --output lib/generated \
--with-db --sync-nullability --db-url "$SUPABASE_DB_URL"
```

`--sync-nullability` reads live column nullability and updates `nullable_fields`
in the contract (preserving comments/formatting). Generation then reads from the
contract as usual. Use `validate --with-db` to *detect* drift without writing.

## The Contract Format

A `supabase.yaml` file is the single source of truth for your backend. A complete,
runnable example lives at [`example/supabase.yaml`](example/supabase.yaml). The
top-level shape:

```yaml
contract:
name: my_project_supabase_contract
version: "0.1.0"
date: "2026-01-01"

# Connection details live under project.remote — name and ref are required.
project:
remote:
name: my_project
ref: abcdefghijklmnop

auth:
provider: supabase
planned_sign_in_methods:
- email

data_model:
public:
things:
ownership: workspace
primary_key: id # honoured by update/delete/stream
fields:
id: uuid
workspace_id: uuid # presence makes the repo workspace-scoped
name: text
category: thing_category
notes: text
nullable_fields:
- notes
enum_values:
thing_category: [device, document, other]
client_access:
select: member_of_workspace
insert: member_of_workspace
update: member_of_workspace
delete: member_of_workspace

storage:
buckets:
avatars:
public: true
allowed_mime_types: [image/png, image/jpeg]
file_size_limit_mb: 5

edge_functions:
runtime: deno # non-mapping keys like this are ignored
resolve_thing:
method: POST
required_request_fields: [query]
optional_request_fields: [workspace_id]

realtime:
publication:
allowed_tables:
- public.things
```

Malformed contracts fail with a path-qualified message
(e.g. `data_model.public.things.primary_key is required`) rather than a stack trace.

### Field Types

| Contract Type | Generated Dart Type |
|---|---|
| `uuid`, `text` | `String` |
| `integer` / `int4`, `bigint` / `int8` | `int` |
| `numeric` / `decimal` | `double` |
| `boolean` / `bool` | `bool` |
| `timestamptz` / `timestamp` / `date` | `DateTime` |
| `jsonb` / `json` | `Map` |
| `vector(N)` | `List` |
| Custom enum | Named Dart enum |

## CLI Reference

### generate

```bash
dart pub global run supabase_client_gen:generate \
--contract \ # Path to supabase.yaml
--output \ # Output directory for generated code
[--check] \ # Verify generated code is up to date (exit 1 if not)
[--with-db] \ # Connect to the DB (only meaningful with --sync-nullability)
[--sync-nullability] \ # Write live-DB nullability back into the contract
[--db-url ] \ # postgres:// connection string (or SUPABASE_DB_URL env)
[--version]
```

### validate

```bash
dart pub global run supabase_client_gen:validate \
--contract \
--output \
[--ts ] \ # Path to supabase.types.ts
[--migrations ] \ # Supabase migrations directory
[--mode=] \ # db | types | ts | migrations | all (default)
[--with-db] [--db-url ] [--json]
```

Four validation checks:
1. **DB ↔ Contract** — tables, columns, types, enums, and nullability aligned?
2. **Contract ↔ Generated Code** — is the Dart client up to date?
3. **Contract ↔ TS Types** — does `supabase.types.ts` match the contract?
4. **Migration Freshness** — any migrations newer than the contract?

## Integration with Melos

```yaml
# melos.yaml
scripts:
gen:client:
run: dart pub global run supabase_client_gen:generate
--contract ../../docs/contracts/supabase.yaml --output lib/generated

gen:client:check:
run: dart pub global run supabase_client_gen:generate
--contract ../../docs/contracts/supabase.yaml --output lib/generated --check

validate:all:
run: dart pub global run supabase_client_gen:validate
--contract ../../docs/contracts/supabase.yaml --output lib/generated
--ts ../../docs/generated/supabase.types.ts
--migrations ../../supabase/migrations --mode=all --with-db
```

## Generated API

### Repository

A workspace-scoped `things` table (it has a `workspace_id` column) yields:

```dart
final repo = ThingRepository(supabase.client);

final things = await repo.select(workspaceId: wsId, limit: 50, offset: 0);
final created = await repo.insert({'workspace_id': wsId, 'name': 'Drill'});
final updated = await repo.update(id, {'name': 'Hammer drill'});
await repo.delete(id);

// Realtime (table listed under realtime.publication.allowed_tables):
repo.stream(workspaceId: wsId).listen((things) => print(things));
```

`update`/`delete`/`stream` use the table's declared `primary_key`. Tables without
a `workspace_id` column get unscoped `select()` / `stream()`. Read-only tables
(all writes `edge_function_only`) get no mutation methods, but still get a
`.stream()` if realtime is enabled.

### Edge function client

Each client-invoked edge function becomes a typed top-level function:

```dart
final result = await resolveThing(query: 'cordless drill', workspaceId: wsId);
// result is Map from the function response
```

### Storage bucket

```dart
final avatars = AvatarsBucket(supabase.client);

await avatars.upload('user/$id.png', bytes, contentType: 'image/png');
final url = avatars.getPublicUrl('user/$id.png'); // signed URL for private buckets
```

Uploads are validated against the bucket's `allowed_mime_types` and
`file_size_limit_mb` before hitting the network.

## Programmatic API

```dart
import 'dart:io';
import 'package:supabase_client_gen/supabase_client_gen.dart';

void main() {
final contract = loadContract('docs/contracts/supabase.yaml');
final files = ClientGenerator(contract).generate();
for (final entry in files.entries) {
File('lib/generated/${entry.key}')
..createSync(recursive: true)
..writeAsStringSync(entry.value);
}
}
```

## License

MIT — see [LICENSE](LICENSE).