Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Polish and improvements #14

Merged
merged 17 commits into from
Jan 1, 2025
Merged
5 changes: 5 additions & 0 deletions .changeset/cuddly-elephants-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@t1mmen/srtd": patch
---

Ensure watcher cleanup on exit
5 changes: 5 additions & 0 deletions .changeset/quiet-seals-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@t1mmen/srtd": minor
---

Allow clearning local, common logs, and resetting of config.
5 changes: 5 additions & 0 deletions .changeset/sharp-owls-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@t1mmen/srtd": minor
---

Greatly improve watch mode stability, while reducing and simplifying the implementation. Add tests.
5 changes: 5 additions & 0 deletions .changeset/tricky-lemons-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@t1mmen/srtd": patch
---

Make CLI "fullscreen"
5 changes: 5 additions & 0 deletions .changeset/wise-fishes-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@t1mmen/srtd": minor
---

Load templates created while `watch` is already running. Restarting no longer necessary
185 changes: 75 additions & 110 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# `srtd` 🪄 Supabase Repeatable Template Definitions

Live-reloading SQL templates for [Supabase](https://supabase.com) projects. DX supercharged! 🚀


> Live-reloading SQL templates for [Supabase](https://supabase.com) projects. DX supercharged! 🚀

[![npm version](https://badge.fury.io/js/@t1mmen%2Fsrtd.svg)](https://www.npmjs.com/package/@t1mmen/srtd)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![CI/CD](https://github.com/t1mmen/srtd/actions/workflows/ci.yml/badge.svg)](https://github.com/t1mmen/srtd/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/t1mmen/srtd/graph/badge.svg?token=CIMAZ55KCJ)](https://codecov.io/gh/t1mmen/srtd)


[![screenshot of srtd](./readme-screenshot.png)](./readme-screenshot.png)

`srtd` enhances the [Supabase](https://supabase.com) DX by adding live-reloading SQL templates into local db. The single-source-of-truth template ➡️ migrations system brings sanity to code reviews, making `git blame` useful.

Built specifically for projects using the standard [Supabase](https://supabase.com) stack (but probably works alright for other Postgres-based projects, too).

**Read the introductory blog post: [Introducing `srtd`: Live-Reloading SQL Templates for Supabase](https://timm.stokke.me/blog/srtd-live-reloading-and-sql-templates-for-supabase)**

## Why This Exists 🤔

While building [Timely](https://www.timely.com)'s next-generation [Memory Engine](https://www.timely.com/memory-app) on [Supabase](https://supabase.com), we found ourselves facing two major annoyances:

1. Code reviews were painful - function changes showed up as complete rewrites rather than helpful diffs
1. Code reviews were painful - function changes showed up as complete rewrites, `git blame` was useless
2. Designing and iterating on database changes locally meant constant friction, like the dance around copy-pasting into SQL console

After over a year of looking-but-not-finding a better way, I paired up with [Claude](https://claude.ai) to eliminate these annoyances. Say hello to `srtd`.
Expand All @@ -29,6 +33,8 @@ After over a year of looking-but-not-finding a better way, I paired up with [Cla
- **Just SQL**: Templates build as standard [Supabase](https://supabase.com) migrations when you're ready to deploy
- **Developer Friendly**: Interactive CLI with visual feedback for all operations

Built specifically for projects using the standard [Supabase](https://supabase.com) stack (but probably works alright for other Postgres-based projects, too).

## Requirements

- Node.js v20.x or higher
Expand Down Expand Up @@ -89,94 +95,94 @@ supabase migration up # Apply using Supabase CLI

Running `srtd` without arguments opens an interactive menu:

```
❯ 🏗️ build - Build Supabase migrations from templates
▶️ apply - Apply migration templates directly to database
✍️ register - Register templates as already built
👀 watch - Watch templates for changes, apply directly to database
```

### CLI Mode

- 🏗️ `build [--force]` - Generate migrations from templates
- ▶️ `apply [--force]` - Apply templates directly to local database
- ✍️ `register [file.sql]` - Mark templates as already built
- 👀 `watch` - Watch and auto-apply changes
- 🏗️ `srtd build [--force]` - Generate migrations from templates
- ▶️ `srtd apply [--force]` - Apply templates directly to local database
- ✍️ `srtd register [file.sql]` - Mark templates as already built
- 👀 `srtd watch` - Watch and auto-apply changes
- 🧹 `srtd clean` - Remove all logs and reset config

> [!IMPORTANT]
> `watch` and `apply` commands modify your local database directly and don't clean up after themselves. Use with caution!

## Perfect For 🎯
## The Power of Templates 💪

### Ideal Use Cases

Templates make code reviews meaningful. Consider this PR adding priority to a notification function:

Without templates, this would appear as a complete rewrite in your PR.

### Perfect For 🎯

✅ Database functions:
```sql
-- Reusable auth helper
CREATE OR REPLACE FUNCTION auth.user_id()
RETURNS uuid AS $$
SELECT auth.uid()::uuid;
$$ LANGUAGE sql SECURITY DEFINER;

-- Event notifications
CREATE OR REPLACE FUNCTION notify_changes()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'changes',
json_build_object('table', TG_TABLE_NAME, 'id', NEW.id)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
```diff
-- Event notifications
CREATE OR REPLACE FUNCTION notify_changes()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'changes',
json_build_object('table', TG_TABLE_NAME, 'id', NEW.id)::text
);
+ RAISE NOTICE 'Notified changes for %', TG_TABLE_NAME; -- Debug logging
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
```

✅ Row-Level Security (RLS):
```sql
-- Replace/update policies safely
DROP POLICY IF EXISTS "workspace_access" ON resources;
CREATE POLICY "workspace_access" ON resources
USING (workspace_id IN (
SELECT id FROM workspaces
WHERE organization_id = auth.organization_id()
));
```diff
-- Replace/update policies safely
DROP POLICY IF EXISTS "workspace_access" ON resources;
CREATE POLICY "workspace_access" ON resources
USING (workspace_id IN (
SELECT id FROM workspaces
WHERE organization_id = auth.organization_id()
+ AND auth.user_role() NOT IN ('pending')
));
```

✅ Views for data abstraction:
```sql
CREATE OR REPLACE VIEW active_subscriptions AS
SELECT
s.*,
p.name as plan_name,
p.features
FROM subscriptions s
JOIN plans p ON p.id = s.plan_id
WHERE s.status = 'active'
AND s.expires_at > CURRENT_TIMESTAMP;
```diff
CREATE OR REPLACE VIEW active_subscriptions AS
SELECT
s.*,
p.name as plan_name,
p.features
FROM subscriptions s
JOIN plans p ON p.id = s.plan_id
- WHERE s.status = 'active';
+ WHERE s.status = 'active'
+ AND s.expires_at > CURRENT_TIMESTAMP;
```

✅ Roles and Permissions:
```sql
-- Revoke all first for clean state
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM public;
```diff
-- Revoke all first for clean state
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM public;

-- Grant specific access
GRANT USAGE ON SCHEMA public TO authenticated;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO authenticated;
-- Grant specific access
GRANT USAGE ON SCHEMA public TO authenticated;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO authenticated;
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO admin;
```

✅ Safe Type Extensions:
```sql
DO $$
BEGIN
-- Add new enum values idempotently
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_type') THEN
CREATE TYPE notification_type AS ENUM ('email', 'sms');
END IF;

-- Extend existing enum safely
ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'push';
END $$;
```diff
DO $$
BEGIN
-- Add new enum values idempotently
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'notification_type') THEN
CREATE TYPE notification_type AS ENUM ('email', 'sms');
END IF;

-- Extend existing enum safely
ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'push';
ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'pusher';
ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'webhook';
+ ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'email';
END $$;
```

### Not Recommended For
Expand All @@ -188,47 +194,6 @@ END $$;

Use regular [Supabase](https://supabase.com) migrations for these cases.

## The Power of Templates 💪

Templates make code reviews meaningful. Consider this PR adding priority to a notification function:

```diff
CREATE OR REPLACE FUNCTION dispatch_notification(
user_id uuid,
type text,
payload jsonb
) RETURNS uuid AS $$
DECLARE
notification_id uuid;
user_settings jsonb;
BEGIN
-- Get user notification settings
SELECT settings INTO user_settings
FROM user_preferences
WHERE id = user_id;

-- Create notification record
+ -- Include priority based on notification type
INSERT INTO notifications (
id,
user_id,
type,
payload,
+ priority,
created_at
) VALUES (
gen_random_uuid(),
dispatch_notification.user_id,
type,
payload,
+ COALESCE((SELECT priority FROM notification_types WHERE name = type), 'normal'),
CURRENT_TIMESTAMP
)
RETURNING id INTO notification_id;
```

Without templates, this would appear as a complete rewrite in your PR.

## Configuration 📝

`srtd.config.json` created during initialization:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dev": "tsc --watch",
"test": "vitest",
"test:coverage": "vitest run --coverage --reporter=junit --outputFile=test-report.junit.xml",
"start": "npm run build && npm link --force && chmod u+x ./dist/cli.js && srtd",
"start": "tsx src/cli.tsx",
"repomix": "mkdir build; npx repomix",
"supabase:start": "npx supabase start",
"supabase:stop": "npx supabase stop"
Expand Down
Binary file added readme-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 40 additions & 27 deletions src/__tests__/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,35 @@ import { connect, disconnect } from '../utils/databaseConnection.js';
export const TEST_FN_PREFIX = 'srtd_scoped_test_func_';
export const TEST_ROOT = join(tmpdir(), `srtd-test-${Date.now()}`);

if (process.env.CI) {
let consoleLogMock: ReturnType<typeof vi.spyOn>;

beforeAll(() => {
consoleLogMock = vi.spyOn(console, 'log').mockImplementation(() => {
// Do nothing
});
});

afterAll(() => {
consoleLogMock.mockRestore();
});
}
vi.mock('../utils/logger', () => ({
logger: {
info: () => {
/** noop */
},
success: () => {
/** noop */
},
warn: () => {
/** noop */
},
error: () => {
/** noop */
},
skip: () => {
/** noop */
},
debug: () => {
/** noop */
},
},
}));

beforeAll(async () => {
await fs.mkdir(TEST_ROOT, { recursive: true });
try {
await fs.mkdir(TEST_ROOT, { recursive: true });
} catch (error) {
console.error('Error creating test root:', error, ', retrying once.');
}
});

afterAll(async () => {
Expand All @@ -33,19 +46,19 @@ afterAll(async () => {
try {
await client.query('BEGIN');
await client.query(`
DO $$
DECLARE
r record;
BEGIN
FOR r IN
SELECT quote_ident(proname) AS func_name
FROM pg_proc
WHERE proname LIKE '${TEST_FN_PREFIX}%'
LOOP
EXECUTE 'DROP FUNCTION IF EXISTS ' || r.func_name;
END LOOP;
END;
$$
DO $$
DECLARE
r record;
BEGIN
FOR r IN
SELECT quote_ident(proname) AS func_name
FROM pg_proc
WHERE proname LIKE '${TEST_FN_PREFIX}%'
LOOP
EXECUTE 'DROP FUNCTION IF EXISTS ' || r.func_name;
END LOOP;
END;
$$;
`);
await client.query('COMMIT');
} catch (e) {
Expand Down
Loading
Loading