@selvajs/supabase-provider 0.12.0 → 0.13.0
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.
- package/README.md +60 -14
- package/dist/cli/sync-migrations.d.ts +3 -0
- package/dist/cli/sync-migrations.d.ts.map +1 -0
- package/dist/cli/sync-migrations.js +108 -0
- package/dist/cli/sync-migrations.js.map +1 -0
- package/dist/data/__tests__/test-helpers.d.ts +2 -3
- package/dist/data/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/data/__tests__/test-helpers.js +1 -0
- package/dist/data/__tests__/test-helpers.js.map +1 -1
- package/dist/data/client.d.ts +10 -2
- package/dist/data/client.d.ts.map +1 -1
- package/dist/data/client.js +3 -0
- package/dist/data/client.js.map +1 -1
- package/package.json +4 -1
- package/src/cli/sync-migrations.ts +133 -0
- package/src/data/__tests__/test-helpers.ts +5 -4
- package/src/data/client.ts +16 -4
- package/supabase/config.toml +7 -2
- package/supabase/migrations/{0001_initial.sql → 20260425155514_selva_initial.sql} +314 -233
- package/supabase/migrations/{0002_definition_version_schema.sql → 20260529202220_selva_definition_version_schema.sql} +1 -1
package/README.md
CHANGED
|
@@ -84,14 +84,16 @@ To switch back to the local provider, swap the import and the three provider lin
|
|
|
84
84
|
|
|
85
85
|
## Applying the schema
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
> **All Selva tables live in a dedicated `selva` schema, not `public`.** This keeps the `public` namespace entirely free for a consuming app's own tables — `selva.projects` and your `public.projects` can coexist. The initial migration creates the schema, grants the standard Supabase roles access, and tells PostgREST to expose it (see [Exposing the `selva` schema to PostgREST](#exposing-the-selva-schema-to-postgrest) for what that means on a hosted project).
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
-
| `0001_initial.sql` | Everything: `user_profiles` (+ auto-seed trigger), `orgs` / `org_members` / `projects` / `project_members` (+ RLS helpers), `definitions` / `definition_versions` (+ deletion-protection FKs and the atomic `increment_run_count` RPC), `invites` (+ `get_invite_by_token` RPC), `compute_servers` (+ per-org/instance defaults, spec §3 BYO compute), `share_links` (+ `try_increment_share_link_solve_count` RPC, spec §7), and the `selva-public` / `selva-private` storage policies. |
|
|
92
|
-
| `0002_definition_version_schema.sql` | Adds `definition_versions.schema` + `schema_extracted_at` (cached compute-extracted UI schema) and the previously-missing `change_note` column. All `add column if not exists`, so it's safe to re-run. |
|
|
89
|
+
The `supabase/migrations/` directory holds timestamp-prefixed files, applied in filename order:
|
|
93
90
|
|
|
94
|
-
|
|
91
|
+
| File | What it installs |
|
|
92
|
+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `20260425155514_selva_initial.sql` | Everything: `user_profiles` (+ auto-seed trigger), `orgs` / `org_members` / `projects` / `project_members` (+ RLS helpers), `definitions` / `definition_versions` (+ deletion-protection FKs and the atomic `increment_run_count` RPC), `invites` (+ `get_invite_by_token` RPC), `compute_servers` (+ per-org/instance defaults, spec §3 BYO compute), `share_links` (+ `try_increment_share_link_solve_count` RPC, spec §7), and the `selva-public` / `selva-private` storage policies. |
|
|
94
|
+
| `20260529202220_selva_definition_version_schema.sql` | Adds `definition_versions.schema` + `schema_extracted_at` (cached compute-extracted UI schema) and the previously-missing `change_note` column. All `add column if not exists`, so it's safe to re-run. |
|
|
95
|
+
|
|
96
|
+
Future schema changes go in new timestamp-prefixed files: `<UTCtimestamp>_selva_<name>.sql` (e.g. `date -u +%Y%m%d%H%M%S` for the prefix). The `_selva_` infix and timestamp prefix let a consuming app's own migrations interleave by time without ever colliding with Selva's — see [Consuming migrations in an external app](#consuming-migrations-in-an-external-app).
|
|
95
97
|
|
|
96
98
|
The `supabase/seed.sql` file creates the two storage buckets (`selva-public`, `selva-private`).
|
|
97
99
|
|
|
@@ -114,14 +116,58 @@ npx supabase db push
|
|
|
114
116
|
|
|
115
117
|
Alternatively you can copy each `.sql` file into Supabase Dashboard → **SQL Editor** and run them in order. The CLI path is strongly preferred — it's idempotent and version-controlled.
|
|
116
118
|
|
|
119
|
+
### Exposing the `selva` schema to PostgREST
|
|
120
|
+
|
|
121
|
+
Because the tables live in `selva` rather than `public`, PostgREST (the REST API supabase-js talks to) must be told to expose that schema, or every query fails with `PGRST106 Invalid schema` / `PGRST205 schema cache`.
|
|
122
|
+
|
|
123
|
+
The initial migration handles this for you: it runs `alter role authenticator set pgrst.db_schemas = '…, selva'` and notifies PostgREST to reload. **You do not list `selva` in `config.toml`'s `[api] schemas`** — PostgREST reads that file at stack boot, before migrations run, so naming a not-yet-created schema there aborts `supabase start`. The migration is the right place because it runs after the schema exists.
|
|
124
|
+
|
|
125
|
+
- **Local dev:** nothing to do — `db reset` / `db push` runs the migration, which exposes `selva` automatically.
|
|
126
|
+
- **Hosted:** the migration still does the work on `db push`. As a belt-and-braces check (or if you applied SQL by hand in the Editor), confirm the schema is exposed under Dashboard → **Project Settings** → **API** → **Exposed schemas** — `selva` should be listed alongside `public` and `graphql_public`. Add it there if it's missing.
|
|
127
|
+
|
|
117
128
|
### Upgrading an existing database
|
|
118
129
|
|
|
119
|
-
|
|
130
|
+
`supabase db push` keys on the version prefix recorded in the project's `supabase_migrations.schema_migrations` table. It applies any file whose prefix isn't recorded yet, in filename order, and skips the rest — so upgrading is always just **re-sync (external app) or re-pull (this repo), then `db push`**. Gaps and out-of-order prefixes are fine; only the lexical filename order matters.
|
|
131
|
+
|
|
132
|
+
No backfill step is required for the schema-caching migration: existing definition versions keep working (the app falls back to fetching their schema from Rhino.Compute), and each version's `schema` column is populated lazily the first time it's solved.
|
|
133
|
+
|
|
134
|
+
#### Migrated from the old `000N_` filenames?
|
|
135
|
+
|
|
136
|
+
Earlier releases shipped `0001_initial.sql` / `0002_definition_version_schema.sql`. These were renamed to timestamp-prefixed names (`20260425155514_selva_initial.sql`, `20260529202220_selva_definition_version_schema.sql`) so they slot into a consuming app's migration timeline without collisions. The **file contents are unchanged** — only the names.
|
|
137
|
+
|
|
138
|
+
To a database that already applied the `000N_` versions, the renamed files look like brand-new migrations. Both are idempotent (`create or replace`, `add column if not exists`), so a stray re-run is a harmless no-op, but you'd end up with duplicate history rows. To reconcile cleanly:
|
|
139
|
+
|
|
140
|
+
- **Local dev stack:** just `npx supabase db reset` — applies the renamed files from scratch.
|
|
141
|
+
- **Hosted project with the old versions applied:** tell the CLI the new versions are already present so it doesn't re-run them:
|
|
142
|
+
```bash
|
|
143
|
+
npx supabase migration repair --status applied 20260425155514 20260529202220
|
|
144
|
+
```
|
|
145
|
+
(Equivalent to inserting those version strings into `supabase_migrations.schema_migrations`.) Then future `db push` runs treat them as done.
|
|
146
|
+
|
|
147
|
+
### Consuming migrations in an external app
|
|
148
|
+
|
|
149
|
+
When you install `@selvajs/supabase-provider` in your own app (rather than running it from this repo), the migration SQL ships inside the package. The clean way to apply it is to **sync the files into your app's own `supabase/migrations/` directory** and let the Supabase CLI drive them alongside your app's own migrations:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# From your app root, after installing/upgrading the package:
|
|
153
|
+
npx selva-supabase sync-migrations # copies into ./supabase/migrations
|
|
154
|
+
npx supabase db push # applies anything not yet recorded
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`sync-migrations` copies each packaged migration **verbatim** (same timestamped filename, same bytes). That's deliberate: the filename is the migration's identity in the history table, so copying without renaming keeps every developer's and CI's history identical.
|
|
158
|
+
|
|
159
|
+
- **`--dir <path>`** — target a non-default migrations directory.
|
|
160
|
+
- **`--force`** — overwrite a same-named file whose content differs (otherwise it's reported as a conflict and the command exits non-zero, leaving your copy untouched).
|
|
161
|
+
- Re-running is idempotent: unchanged files are skipped.
|
|
162
|
+
|
|
163
|
+
**Upgrade flow** (running app, you bump the provider and it carries a new migration): `pnpm up @selvajs/supabase-provider`, then `npx selva-supabase sync-migrations` copies in the new file (later timestamp → sorts last), then `npx supabase db push` applies only that file and skips the ones already recorded. No renumbering, even if your app added its own migrations in between.
|
|
120
164
|
|
|
121
|
-
|
|
122
|
-
- **SQL Editor:** paste the contents of `0002_definition_version_schema.sql` and run it. It uses `add column if not exists`, so running it on an already-migrated DB is a safe no-op.
|
|
165
|
+
**Notes specific to consuming in an external app:**
|
|
123
166
|
|
|
124
|
-
|
|
167
|
+
- **Your tables stay in `public`; Selva's are in `selva`.** Because the engine owns a separate schema, there are no name clashes — your app can have its own `public.projects`, `public.orgs`, `public.definitions`, etc. without touching Selva's. Nothing to check before the first `db push`.
|
|
168
|
+
- **Referencing engine objects from your own migrations.** When your app's tables need to point at the engine (FKs, RLS helpers), qualify them with the `selva` schema: `references selva.orgs(id)`, `selva.is_org_member(org_id)`, `selva.is_instance_admin()`, `selva.set_updated_at()`. The qualified names are self-documenting and won't accidentally resolve to a same-named object in your `public`.
|
|
169
|
+
- **Expose `selva` in your app's PostgREST too.** If your app uses its own Supabase CLI project / `config.toml`, the synced initial migration exposes `selva` via `alter role authenticator …` when it runs — same mechanism as above. On a hosted project, verify it under Project Settings → API → Exposed schemas after the first push.
|
|
170
|
+
- **Storage buckets aren't created by `db push` on a hosted project.** `seed.sql` (which creates `selva-public` / `selva-private`) is dev-only. On a hosted project, run it once by hand in the SQL Editor after the first push — same as the [Production](#production--hosted-supabase-project) flow.
|
|
125
171
|
|
|
126
172
|
---
|
|
127
173
|
|
|
@@ -187,7 +233,7 @@ Studio is a full admin UI at `http://127.0.0.1:54323`. Use it to inspect tables,
|
|
|
187
233
|
6. Deploy the selva app with `selva.config.ts` wired to `@selvajs/supabase-provider`.
|
|
188
234
|
7. Bootstrap the first user:
|
|
189
235
|
- Open `/setup` once — creates the first admin with `instance_admin`.
|
|
190
|
-
- Or manually: Dashboard → **Authentication** → **Add user**, then in the SQL Editor: `UPDATE
|
|
236
|
+
- Or manually: Dashboard → **Authentication** → **Add user**, then in the SQL Editor: `UPDATE selva.user_profiles SET platform_permissions = ARRAY['instance_admin']::text[] WHERE user_id = '<uuid>';`
|
|
191
237
|
|
|
192
238
|
### Operational checklist
|
|
193
239
|
|
|
@@ -245,13 +291,13 @@ MFA methods on `IPasswordAuth` are currently undefined — MFA is deferred. If a
|
|
|
245
291
|
|
|
246
292
|
### Data + RLS
|
|
247
293
|
|
|
248
|
-
Every store goes through `ClientBundle.forRequest(ctx)`:
|
|
294
|
+
Every client is constructed with `db: { schema: 'selva' }`, so a bare `client.from('orgs')` resolves to `selva.orgs`. Every store goes through `ClientBundle.forRequest(ctx)`:
|
|
249
295
|
|
|
250
296
|
- `ctx.system` → service-role client (bypasses RLS)
|
|
251
297
|
- `ctx.adapterContext.sessionToken` → anon client with `Authorization: Bearer <jwt>` (RLS enforces per-user visibility)
|
|
252
|
-
- neither →
|
|
298
|
+
- neither → anon client (RLS active, no `auth.uid()`) — fail-closed. Service-role is opt-in via `ctx.system`, never derived from a missing session.
|
|
253
299
|
|
|
254
|
-
Helper SQL functions (`is_instance_admin`, `is_org_member`, `visible_project`, `has_org_permission`) are `SECURITY DEFINER` so they evaluate without looping through RLS.
|
|
300
|
+
Helper SQL functions (`selva.is_instance_admin`, `selva.is_org_member`, `selva.visible_project`, `selva.has_org_permission`) are `SECURITY DEFINER` with `search_path = selva, public, extensions` so they evaluate without looping through RLS.
|
|
255
301
|
|
|
256
302
|
### User profile
|
|
257
303
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-migrations.d.ts","sourceRoot":"","sources":["../../src/cli/sync-migrations.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// sync-migrations — copy this package's Supabase migrations into a consuming
|
|
4
|
+
// app's supabase/migrations/ directory.
|
|
5
|
+
//
|
|
6
|
+
// Migrations ship with deterministic timestamp-prefixed filenames
|
|
7
|
+
// (<UTCtimestamp>_selva_<name>.sql) so they slot into a consuming app's own
|
|
8
|
+
// migration timeline without colliding, and so every machine/CI produces a
|
|
9
|
+
// byte-identical migration history. We copy verbatim — no rename at copy time,
|
|
10
|
+
// which is what keeps the history stable.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]
|
|
14
|
+
//
|
|
15
|
+
// --dir <path> Target migrations dir (default: ./supabase/migrations)
|
|
16
|
+
// --force Overwrite files that exist with differing content
|
|
17
|
+
// ============================================================================
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
// dist/cli/ -> package root is two levels up; migrations ship alongside src/dist.
|
|
23
|
+
const packageRoot = resolve(here, '..', '..');
|
|
24
|
+
const sourceDir = join(packageRoot, 'supabase', 'migrations');
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
let targetDir = resolve(process.cwd(), 'supabase', 'migrations');
|
|
27
|
+
let force = false;
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const arg = argv[i];
|
|
30
|
+
if (arg === '--dir') {
|
|
31
|
+
const value = argv[++i];
|
|
32
|
+
if (!value)
|
|
33
|
+
fail('--dir requires a path argument');
|
|
34
|
+
targetDir = resolve(process.cwd(), value);
|
|
35
|
+
}
|
|
36
|
+
else if (arg === '--force') {
|
|
37
|
+
force = true;
|
|
38
|
+
}
|
|
39
|
+
else if (arg === '--help' || arg === '-h') {
|
|
40
|
+
printHelp();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
fail(`unknown argument: ${arg}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { targetDir, force };
|
|
48
|
+
}
|
|
49
|
+
function printHelp() {
|
|
50
|
+
console.log([
|
|
51
|
+
'sync-migrations — copy @selvajs/supabase-provider migrations into your app.',
|
|
52
|
+
'',
|
|
53
|
+
'Usage:',
|
|
54
|
+
' npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]',
|
|
55
|
+
'',
|
|
56
|
+
' --dir <path> Target migrations dir (default: ./supabase/migrations)',
|
|
57
|
+
' --force Overwrite files that exist with differing content',
|
|
58
|
+
'',
|
|
59
|
+
'After syncing, apply with: npx supabase db push'
|
|
60
|
+
].join('\n'));
|
|
61
|
+
}
|
|
62
|
+
function fail(message) {
|
|
63
|
+
console.error(`error: ${message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
function main() {
|
|
67
|
+
const { targetDir, force } = parseArgs(process.argv.slice(2));
|
|
68
|
+
if (!existsSync(sourceDir)) {
|
|
69
|
+
fail(`could not locate packaged migrations at ${sourceDir}`);
|
|
70
|
+
}
|
|
71
|
+
const files = readdirSync(sourceDir)
|
|
72
|
+
.filter((name) => name.endsWith('.sql'))
|
|
73
|
+
.sort();
|
|
74
|
+
if (files.length === 0) {
|
|
75
|
+
fail(`no .sql migrations found in ${sourceDir}`);
|
|
76
|
+
}
|
|
77
|
+
mkdirSync(targetDir, { recursive: true });
|
|
78
|
+
let copied = 0;
|
|
79
|
+
let skipped = 0;
|
|
80
|
+
let conflicts = 0;
|
|
81
|
+
for (const name of files) {
|
|
82
|
+
const from = join(sourceDir, name);
|
|
83
|
+
const to = join(targetDir, name);
|
|
84
|
+
const contents = readFileSync(from);
|
|
85
|
+
if (existsSync(to)) {
|
|
86
|
+
const existing = readFileSync(to);
|
|
87
|
+
if (existing.equals(contents)) {
|
|
88
|
+
skipped++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!force) {
|
|
92
|
+
console.warn(` conflict ${name} exists with different content (use --force to overwrite)`);
|
|
93
|
+
conflicts++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
writeFileSync(to, contents);
|
|
98
|
+
console.log(` copied ${name}`);
|
|
99
|
+
copied++;
|
|
100
|
+
}
|
|
101
|
+
console.log(`\n${copied} copied, ${skipped} unchanged, ${conflicts} conflict(s) -> ${targetDir}`);
|
|
102
|
+
if (conflicts > 0) {
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
console.log('\nNext: review the files, then run npx supabase db push');
|
|
106
|
+
}
|
|
107
|
+
main();
|
|
108
|
+
//# sourceMappingURL=sync-migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-migrations.js","sourceRoot":"","sources":["../../src/cli/sync-migrations.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
* service-role client for both — seeding users in `auth.users` and wiping
|
|
6
6
|
* every relevant table.
|
|
7
7
|
*/
|
|
8
|
-
import { type
|
|
9
|
-
import { type ClientBundle } from '../client.js';
|
|
8
|
+
import { type ClientBundle, type SelvaSchemaClient } from '../client.js';
|
|
10
9
|
export interface TestContext {
|
|
11
10
|
url: string;
|
|
12
11
|
anonKey: string;
|
|
13
12
|
serviceRoleKey: string;
|
|
14
|
-
adminClient:
|
|
13
|
+
adminClient: SelvaSchemaClient;
|
|
15
14
|
bundle: ClientBundle;
|
|
16
15
|
}
|
|
17
16
|
export declare function readEnv(): TestContext | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../../src/data/__tests__/test-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../../src/data/__tests__/test-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAqB,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAE5F,MAAM,WAAW,WAAW;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,MAAM,EAAE,YAAY,CAAC;CACrB;AAED,wBAAgB,OAAO,IAAI,WAAW,GAAG,IAAI,CAuB5C;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlE;AAuCD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,CAC7B,GAAG,EAAE,WAAW,EAChB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAEnD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAClC,GAAG,EAAE,WAAW,EAChB,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAEnD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../../../src/data/__tests__/test-helpers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../../../src/data/__tests__/test-helpers.ts"],"names":[],"mappings":"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"}
|
package/dist/data/client.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import type { RequestContext } from '@selvajs/platform';
|
|
3
|
+
/**
|
|
4
|
+
* All engine tables live in the `selva` schema (not `public`) so a consuming
|
|
5
|
+
* app keeps `public` for its own tables. Every data client targets `selva` via
|
|
6
|
+
* `db: { schema: 'selva' }`, which the supabase-js type system surfaces as the
|
|
7
|
+
* client's schema generic — hence this alias rather than the bare default
|
|
8
|
+
* `SupabaseClient` (which is pinned to `'public'`).
|
|
9
|
+
*/
|
|
10
|
+
export type SelvaSchemaClient = SupabaseClient<any, 'public', 'selva'>;
|
|
3
11
|
/**
|
|
4
12
|
* Build a Supabase client for a given `RequestContext`.
|
|
5
13
|
*
|
|
@@ -26,14 +34,14 @@ import type { RequestContext } from '@selvajs/platform';
|
|
|
26
34
|
*/
|
|
27
35
|
export interface ClientBundle {
|
|
28
36
|
/** Long-lived service-role client. Bypasses RLS. */
|
|
29
|
-
serviceClient:
|
|
37
|
+
serviceClient: SelvaSchemaClient;
|
|
30
38
|
/**
|
|
31
39
|
* Produce (or reuse) a client scoped to this request's identity.
|
|
32
40
|
* - `ctx.system === true` → service-role (RLS bypassed).
|
|
33
41
|
* - `ctx.adapterContext.sessionToken` set → user-scoped (RLS active).
|
|
34
42
|
* - neither → anon client (RLS active, no `auth.uid()`).
|
|
35
43
|
*/
|
|
36
|
-
forRequest(ctx: RequestContext):
|
|
44
|
+
forRequest(ctx: RequestContext): SelvaSchemaClient;
|
|
37
45
|
}
|
|
38
46
|
export interface BuildClientOptions {
|
|
39
47
|
supabaseUrl: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/data/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,YAAY;IAC5B,oDAAoD;IACpD,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/data/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,YAAY;IAC5B,oDAAoD;IACpD,aAAa,EAAE,iBAAiB,CAAC;IACjC;;;;;OAKG;IACH,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,iBAAiB,CAAC;CACnD;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,YAAY,CAqCxE"}
|
package/dist/data/client.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createClient } from '@supabase/supabase-js';
|
|
2
2
|
export function buildClientBundle(opts) {
|
|
3
3
|
const serviceClient = createClient(opts.supabaseUrl, opts.serviceRoleKey, {
|
|
4
|
+
db: { schema: 'selva' },
|
|
4
5
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
5
6
|
});
|
|
6
7
|
// Long-lived anon client — the fail-closed default for ctxes that aren't
|
|
7
8
|
// explicitly system and don't carry a session token.
|
|
8
9
|
const anonClient = createClient(opts.supabaseUrl, opts.anonKey, {
|
|
10
|
+
db: { schema: 'selva' },
|
|
9
11
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
10
12
|
});
|
|
11
13
|
const perRequestCache = new WeakMap();
|
|
@@ -21,6 +23,7 @@ export function buildClientBundle(opts) {
|
|
|
21
23
|
if (cached)
|
|
22
24
|
return cached;
|
|
23
25
|
const client = createClient(opts.supabaseUrl, opts.anonKey, {
|
|
26
|
+
db: { schema: 'selva' },
|
|
24
27
|
auth: { persistSession: false, autoRefreshToken: false },
|
|
25
28
|
global: {
|
|
26
29
|
headers: { Authorization: `Bearer ${token}` }
|
package/dist/data/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/data/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/data/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,uBAAuB,CAAC;AAsD1E,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACzD,MAAM,aAAa,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE;QACzE,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QACvB,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE;KACxD,CAAC,CAAC;IAEH,yEAAyE;IACzE,qDAAqD;IACrD,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE;QAC/D,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QACvB,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE;KACxD,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,IAAI,OAAO,EAAqC,CAAC;IAEzE,OAAO;QACN,aAAa;QACb,UAAU,CAAC,GAAmB;YAC7B,IAAI,GAAG,CAAC,MAAM;gBAAE,OAAO,aAAa,CAAC;YAErC,MAAM,KAAK,GAAG,mBAAmB,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACtD,IAAI,CAAC,KAAK;gBAAE,OAAO,UAAU,CAAC;YAE9B,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACxC,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAC;YAE1B,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,EAAE;gBAC3D,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;gBACvB,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE;gBACxD,MAAM,EAAE;oBACP,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;iBAC7C;aACD,CAAC,CAAC;YACH,eAAe,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACjC,OAAO,MAAM,CAAC;QACf,CAAC;KACD,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,cAAuB;IACnD,IAAI,CAAC,cAAc,IAAI,OAAO,cAAc,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC5E,MAAM,KAAK,GAAI,cAA6C,CAAC,YAAY,CAAC;IAC1E,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1E,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/supabase-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Supabase (Auth + Postgres + Storage) implementations of @selvajs/platform interfaces",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "./dist/index.js",
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"selva-supabase": "./dist/cli/sync-migrations.js"
|
|
19
|
+
},
|
|
17
20
|
"exports": {
|
|
18
21
|
".": {
|
|
19
22
|
"source": "./src/index.ts",
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// sync-migrations — copy this package's Supabase migrations into a consuming
|
|
4
|
+
// app's supabase/migrations/ directory.
|
|
5
|
+
//
|
|
6
|
+
// Migrations ship with deterministic timestamp-prefixed filenames
|
|
7
|
+
// (<UTCtimestamp>_selva_<name>.sql) so they slot into a consuming app's own
|
|
8
|
+
// migration timeline without colliding, and so every machine/CI produces a
|
|
9
|
+
// byte-identical migration history. We copy verbatim — no rename at copy time,
|
|
10
|
+
// which is what keeps the history stable.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]
|
|
14
|
+
//
|
|
15
|
+
// --dir <path> Target migrations dir (default: ./supabase/migrations)
|
|
16
|
+
// --force Overwrite files that exist with differing content
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
+
|
|
23
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
|
|
25
|
+
// dist/cli/ -> package root is two levels up; migrations ship alongside src/dist.
|
|
26
|
+
const packageRoot = resolve(here, '..', '..');
|
|
27
|
+
const sourceDir = join(packageRoot, 'supabase', 'migrations');
|
|
28
|
+
|
|
29
|
+
interface Options {
|
|
30
|
+
targetDir: string;
|
|
31
|
+
force: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv: string[]): Options {
|
|
35
|
+
let targetDir = resolve(process.cwd(), 'supabase', 'migrations');
|
|
36
|
+
let force = false;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const arg = argv[i];
|
|
40
|
+
if (arg === '--dir') {
|
|
41
|
+
const value = argv[++i];
|
|
42
|
+
if (!value) fail('--dir requires a path argument');
|
|
43
|
+
targetDir = resolve(process.cwd(), value);
|
|
44
|
+
} else if (arg === '--force') {
|
|
45
|
+
force = true;
|
|
46
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
47
|
+
printHelp();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
} else {
|
|
50
|
+
fail(`unknown argument: ${arg}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { targetDir, force };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function printHelp(): void {
|
|
58
|
+
console.log(
|
|
59
|
+
[
|
|
60
|
+
'sync-migrations — copy @selvajs/supabase-provider migrations into your app.',
|
|
61
|
+
'',
|
|
62
|
+
'Usage:',
|
|
63
|
+
' npx @selvajs/supabase-provider sync-migrations [--dir <path>] [--force]',
|
|
64
|
+
'',
|
|
65
|
+
' --dir <path> Target migrations dir (default: ./supabase/migrations)',
|
|
66
|
+
' --force Overwrite files that exist with differing content',
|
|
67
|
+
'',
|
|
68
|
+
'After syncing, apply with: npx supabase db push'
|
|
69
|
+
].join('\n')
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fail(message: string): never {
|
|
74
|
+
console.error(`error: ${message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main(): void {
|
|
79
|
+
const { targetDir, force } = parseArgs(process.argv.slice(2));
|
|
80
|
+
|
|
81
|
+
if (!existsSync(sourceDir)) {
|
|
82
|
+
fail(`could not locate packaged migrations at ${sourceDir}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const files = readdirSync(sourceDir)
|
|
86
|
+
.filter((name) => name.endsWith('.sql'))
|
|
87
|
+
.sort();
|
|
88
|
+
|
|
89
|
+
if (files.length === 0) {
|
|
90
|
+
fail(`no .sql migrations found in ${sourceDir}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
mkdirSync(targetDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
let copied = 0;
|
|
96
|
+
let skipped = 0;
|
|
97
|
+
let conflicts = 0;
|
|
98
|
+
|
|
99
|
+
for (const name of files) {
|
|
100
|
+
const from = join(sourceDir, name);
|
|
101
|
+
const to = join(targetDir, name);
|
|
102
|
+
const contents = readFileSync(from);
|
|
103
|
+
|
|
104
|
+
if (existsSync(to)) {
|
|
105
|
+
const existing = readFileSync(to);
|
|
106
|
+
if (existing.equals(contents)) {
|
|
107
|
+
skipped++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!force) {
|
|
111
|
+
console.warn(
|
|
112
|
+
` conflict ${name} exists with different content (use --force to overwrite)`
|
|
113
|
+
);
|
|
114
|
+
conflicts++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
writeFileSync(to, contents);
|
|
120
|
+
console.log(` copied ${name}`);
|
|
121
|
+
copied++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`\n${copied} copied, ${skipped} unchanged, ${conflicts} conflict(s) -> ${targetDir}`);
|
|
125
|
+
|
|
126
|
+
if (conflicts > 0) {
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log('\nNext: review the files, then run npx supabase db push');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main();
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* every relevant table.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { createClient
|
|
10
|
-
import { buildClientBundle, type ClientBundle } from '../client.js';
|
|
9
|
+
import { createClient } from '@supabase/supabase-js';
|
|
10
|
+
import { buildClientBundle, type ClientBundle, type SelvaSchemaClient } from '../client.js';
|
|
11
11
|
|
|
12
12
|
export interface TestContext {
|
|
13
13
|
url: string;
|
|
14
14
|
anonKey: string;
|
|
15
15
|
serviceRoleKey: string;
|
|
16
|
-
adminClient:
|
|
16
|
+
adminClient: SelvaSchemaClient;
|
|
17
17
|
bundle: ClientBundle;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -27,9 +27,10 @@ export function readEnv(): TestContext | null {
|
|
|
27
27
|
// loads `@supabase/realtime-js`, which on Node < 22 throws at construction
|
|
28
28
|
// without a native WebSocket. Swallow that so the suite cleanly skips
|
|
29
29
|
// instead of crashing test collection.
|
|
30
|
-
let adminClient:
|
|
30
|
+
let adminClient: SelvaSchemaClient;
|
|
31
31
|
try {
|
|
32
32
|
adminClient = createClient(url, serviceRoleKey, {
|
|
33
|
+
db: { schema: 'selva' },
|
|
33
34
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
34
35
|
});
|
|
35
36
|
} catch (err) {
|
package/src/data/client.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import type { RequestContext } from '@selvajs/platform';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* All engine tables live in the `selva` schema (not `public`) so a consuming
|
|
6
|
+
* app keeps `public` for its own tables. Every data client targets `selva` via
|
|
7
|
+
* `db: { schema: 'selva' }`, which the supabase-js type system surfaces as the
|
|
8
|
+
* client's schema generic — hence this alias rather than the bare default
|
|
9
|
+
* `SupabaseClient` (which is pinned to `'public'`).
|
|
10
|
+
*/
|
|
11
|
+
export type SelvaSchemaClient = SupabaseClient<any, 'public', 'selva'>;
|
|
12
|
+
|
|
4
13
|
/**
|
|
5
14
|
* Build a Supabase client for a given `RequestContext`.
|
|
6
15
|
*
|
|
@@ -27,14 +36,14 @@ import type { RequestContext } from '@selvajs/platform';
|
|
|
27
36
|
*/
|
|
28
37
|
export interface ClientBundle {
|
|
29
38
|
/** Long-lived service-role client. Bypasses RLS. */
|
|
30
|
-
serviceClient:
|
|
39
|
+
serviceClient: SelvaSchemaClient;
|
|
31
40
|
/**
|
|
32
41
|
* Produce (or reuse) a client scoped to this request's identity.
|
|
33
42
|
* - `ctx.system === true` → service-role (RLS bypassed).
|
|
34
43
|
* - `ctx.adapterContext.sessionToken` set → user-scoped (RLS active).
|
|
35
44
|
* - neither → anon client (RLS active, no `auth.uid()`).
|
|
36
45
|
*/
|
|
37
|
-
forRequest(ctx: RequestContext):
|
|
46
|
+
forRequest(ctx: RequestContext): SelvaSchemaClient;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
export interface BuildClientOptions {
|
|
@@ -45,20 +54,22 @@ export interface BuildClientOptions {
|
|
|
45
54
|
|
|
46
55
|
export function buildClientBundle(opts: BuildClientOptions): ClientBundle {
|
|
47
56
|
const serviceClient = createClient(opts.supabaseUrl, opts.serviceRoleKey, {
|
|
57
|
+
db: { schema: 'selva' },
|
|
48
58
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
49
59
|
});
|
|
50
60
|
|
|
51
61
|
// Long-lived anon client — the fail-closed default for ctxes that aren't
|
|
52
62
|
// explicitly system and don't carry a session token.
|
|
53
63
|
const anonClient = createClient(opts.supabaseUrl, opts.anonKey, {
|
|
64
|
+
db: { schema: 'selva' },
|
|
54
65
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
55
66
|
});
|
|
56
67
|
|
|
57
|
-
const perRequestCache = new WeakMap<RequestContext,
|
|
68
|
+
const perRequestCache = new WeakMap<RequestContext, SelvaSchemaClient>();
|
|
58
69
|
|
|
59
70
|
return {
|
|
60
71
|
serviceClient,
|
|
61
|
-
forRequest(ctx: RequestContext):
|
|
72
|
+
forRequest(ctx: RequestContext): SelvaSchemaClient {
|
|
62
73
|
if (ctx.system) return serviceClient;
|
|
63
74
|
|
|
64
75
|
const token = extractSessionToken(ctx.adapterContext);
|
|
@@ -68,6 +79,7 @@ export function buildClientBundle(opts: BuildClientOptions): ClientBundle {
|
|
|
68
79
|
if (cached) return cached;
|
|
69
80
|
|
|
70
81
|
const client = createClient(opts.supabaseUrl, opts.anonKey, {
|
|
82
|
+
db: { schema: 'selva' },
|
|
71
83
|
auth: { persistSession: false, autoRefreshToken: false },
|
|
72
84
|
global: {
|
|
73
85
|
headers: { Authorization: `Bearer ${token}` }
|