@guard-rail/core 0.2.1 → 0.5.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 +132 -31
- package/dist/adapters/supabase/auth.js +50 -0
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/adapters/supabase/db.js +11 -0
- package/dist/adapters/supabase/db.js.map +1 -1
- package/dist/cli/init-db.d.ts +1 -0
- package/dist/cli/init-db.js +121 -0
- package/dist/cli/init-db.js.map +1 -0
- package/dist/core/adapter-types.d.ts +77 -1
- package/dist/core/config.d.ts +7 -1
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/defs.d.ts +43 -2
- package/dist/core/defs.js +29 -2
- package/dist/core/defs.js.map +1 -1
- package/dist/core/scaffold.d.ts +11 -0
- package/dist/core/scaffold.js +19 -0
- package/dist/core/scaffold.js.map +1 -0
- package/dist/core/types.d.ts +21 -1
- package/dist/index.client.d.ts +18 -0
- package/dist/index.client.js +70 -0
- package/dist/index.client.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.d.ts +2 -2
- package/dist/react/Admin.js +84 -8
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthRedirectSync.d.ts +20 -0
- package/dist/react/AuthRedirectSync.js +38 -0
- package/dist/react/AuthRedirectSync.js.map +1 -0
- package/dist/react/GRail/client.js +5 -1
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/GRail/editor.d.ts +24 -0
- package/dist/react/GRail/editor.js +9 -0
- package/dist/react/GRail/editor.js.map +1 -0
- package/dist/react/GRail/index.js +2 -2
- package/dist/react/GRail/index.js.map +1 -1
- package/dist/react/GRail/render.d.ts +3 -3
- package/dist/react/GRail/universal.d.ts +22 -0
- package/dist/react/GRail/universal.js +29 -0
- package/dist/react/GRail/universal.js.map +1 -0
- package/dist/react/Provider.d.ts +9 -5
- package/dist/react/Provider.js +15 -5
- package/dist/react/Provider.js.map +1 -1
- package/dist/react/Repeater/client.d.ts +3 -3
- package/dist/react/Repeater/index.d.ts +3 -3
- package/dist/react/Syncer.d.ts +10 -8
- package/dist/react/Syncer.js +8 -6
- package/dist/react/Syncer.js.map +1 -1
- package/dist/react/Team.js +1 -1
- package/dist/react/Team.js.map +1 -1
- package/dist/react/useAnchorRect.d.ts +17 -1
- package/dist/react/useAnchorRect.js +37 -9
- package/dist/react/useAnchorRect.js.map +1 -1
- package/dist/server/actions.d.ts +31 -3
- package/dist/server/actions.js +45 -1
- package/dist/server/actions.js.map +1 -1
- package/dist/server/data.js +1 -15
- package/dist/server/data.js.map +1 -1
- package/dist/server/loader.d.ts +9 -2
- package/dist/server/loader.js +74 -11
- package/dist/server/loader.js.map +1 -1
- package/dist/server/wrapper.d.ts +2 -2
- package/dist/server/wrapper.js +13 -4
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +12 -2
- package/scripts/postinstall.mjs +4 -4
package/README.md
CHANGED
|
@@ -25,14 +25,20 @@ pnpm add @guard-rail/core @supabase/ssr @supabase/supabase-js
|
|
|
25
25
|
|
|
26
26
|
Set up the database:
|
|
27
27
|
|
|
28
|
-
1.
|
|
29
|
-
project's `supabase/migrations
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
1. Run `npx guardrail-init-db --push`. It copies the package's migration(s)
|
|
29
|
+
into your own project's `supabase/migrations/` (skipping any already
|
|
30
|
+
there — safe to re-run after upgrading), runs `supabase init` if needed,
|
|
31
|
+
adds `guardrail` to `config.toml`'s exposed schemas for local dev, and
|
|
32
|
+
applies the migration with `supabase db push`. Leave off `--push` to copy
|
|
33
|
+
without applying. No Supabase CLI at all? Paste the migration file's
|
|
34
|
+
contents into the SQL editor instead — same statements, just without the
|
|
35
|
+
CLI's version tracking.
|
|
36
|
+
2. **Required for a hosted project:** also add `guardrail` under
|
|
37
|
+
**Settings > API > Exposed schemas** in the Supabase dashboard —
|
|
38
|
+
`config.toml`'s schemas list only governs local dev. PostgREST only
|
|
39
|
+
serves schemas on that list; every table below is otherwise invisible to
|
|
40
|
+
the client with a "not found in schema cache" error that looks identical
|
|
41
|
+
to a missing table.
|
|
36
42
|
|
|
37
43
|
Guardrail's tables live in their own `guardrail` Postgres schema, not `public`
|
|
38
44
|
— so a `supabase db reset`, a broad `DROP ... CASCADE`, or a migration tool
|
|
@@ -107,8 +113,72 @@ import { GRail } from "@guard-rail/core";
|
|
|
107
113
|
`id` is the content key. `className` and every other prop pass straight through
|
|
108
114
|
to the element — Guardrail never styles your content.
|
|
109
115
|
|
|
116
|
+
Links take `as="a"` (so `href` type-checks) and edit both the text and the URL:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<GRail id="nav.cta" as="a" type="link" href="/contact">Get in touch</GRail>
|
|
120
|
+
```
|
|
121
|
+
|
|
110
122
|
To edit, visit any page with `?edit=true`, sign in with a Supabase user, and
|
|
111
|
-
click a field.
|
|
123
|
+
click a field. `?edit=true` itself only grants a short (~10 minute) window to
|
|
124
|
+
reach the sign-in form — a normal visitor never sees any Guardrail chrome, and
|
|
125
|
+
a stray copy of that window (a shared computer, an old tab) self-expires
|
|
126
|
+
rather than showing a sign-in bar indefinitely. It's a dismissable prompt, not
|
|
127
|
+
a standing invitation: a signed-in editor's toolbar isn't on that timer at
|
|
128
|
+
all, and persists for as long as their real session does, with no need to
|
|
129
|
+
revisit `?edit=true` again after the first sign-in. `?edit=false` explicitly
|
|
130
|
+
hides the toolbar (also for ~10 minutes) if you want to preview the site
|
|
131
|
+
without it while still signed in.
|
|
132
|
+
|
|
133
|
+
### In Client Components
|
|
134
|
+
|
|
135
|
+
`<GRail>` works the same inside a `"use client"` component — a mobile-nav header,
|
|
136
|
+
a carousel, an accordion — with the same import and the same props:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
"use client";
|
|
140
|
+
import { GRail } from "@guard-rail/core";
|
|
141
|
+
|
|
142
|
+
export function Header() {
|
|
143
|
+
const [open, setOpen] = useState(false);
|
|
144
|
+
return (
|
|
145
|
+
<header>
|
|
146
|
+
<GRail id="header.tagline" as="p">Built by hand in Oklahoma</GRail>
|
|
147
|
+
<button onClick={() => setOpen(!open)}>Menu</button>
|
|
148
|
+
</header>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The package resolves `@guard-rail/core` to an async Server Component in server
|
|
154
|
+
code and to a client-safe version in client code (via the `"react-server"` export
|
|
155
|
+
condition), so moving a component between the two needs no changes. Edited
|
|
156
|
+
content is still in the server-rendered HTML — a Client Component is rendered on
|
|
157
|
+
the server for its first paint too — so SEO is unaffected.
|
|
158
|
+
|
|
159
|
+
A Client Component can't await a query, so instead the layout **preloads** the
|
|
160
|
+
site's content once per request and hands it down. That preload is the same
|
|
161
|
+
single query the server-rendered fields read from, so there's no extra cost: one
|
|
162
|
+
query per request, whether a page uses server fields, client fields, or both.
|
|
163
|
+
|
|
164
|
+
By default it preloads up to 500 rows with no configuration. If a site outgrows
|
|
165
|
+
that, a dev-mode warning says so — scope it, or turn it off:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
withGuardrail(RootLayout, {
|
|
169
|
+
...supabase,
|
|
170
|
+
prefetch: { prefix: ["home.", "header."] }, // only preload these; optional
|
|
171
|
+
// prefetch: { limit: 2000 }, // or raise the ceiling
|
|
172
|
+
// prefetch: false, // off: client fields show their JSX
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
What still needs a Server Component: `<GRepeater>`, `getFieldValue()`, and
|
|
177
|
+
`listRepeaterItems()` throw a clear error if used from client code. Render them in
|
|
178
|
+
a Server Component and pass the result into the client one as `children` or a
|
|
179
|
+
prop. If your tooling doesn't set the `"react-server"` condition (a test runner,
|
|
180
|
+
a plain Node script), import from `@guard-rail/core/server` or
|
|
181
|
+
`@guard-rail/core/client` explicitly.
|
|
112
182
|
|
|
113
183
|
## Team and roles
|
|
114
184
|
|
|
@@ -136,26 +206,42 @@ export default withGuardrail(RootLayout, {
|
|
|
136
206
|
});
|
|
137
207
|
```
|
|
138
208
|
|
|
139
|
-
**Invite emails (Supabase).** Inviting someone
|
|
140
|
-
|
|
141
|
-
|
|
209
|
+
**Invite emails (Supabase).** Inviting someone emails them a sign-in link
|
|
210
|
+
that creates their account on first use — this works out of the box, no
|
|
211
|
+
route or email template changes needed. A small always-mounted component
|
|
212
|
+
notices the link's tokens (in the URL hash, a `code`, or a `token_hash` —
|
|
213
|
+
whichever style your project's templates happen to produce, including the
|
|
214
|
+
unedited default) and completes the session automatically.
|
|
215
|
+
|
|
216
|
+
Someone arriving this way has a valid session but no password yet, so they
|
|
217
|
+
get a **Set Password** prompt in place of the usual toolbar — skippable,
|
|
218
|
+
since a magic link works fine on its own indefinitely if they'd rather not.
|
|
219
|
+
The same prompt appears after a password-recovery link.
|
|
220
|
+
|
|
221
|
+
Two things still worth knowing:
|
|
222
|
+
|
|
223
|
+
- **Auth → URL Configuration**, in the Supabase dashboard: Site URL and
|
|
224
|
+
Redirect URLs must actually match wherever your app runs, including in
|
|
225
|
+
local dev — a mismatch here makes Supabase silently redirect to whatever
|
|
226
|
+
Site URL *is* configured instead of erroring, which reads exactly like the
|
|
227
|
+
link "not working" for no visible reason.
|
|
228
|
+
- **Auth → SMTP**: Supabase's built-in mailer allows only a few emails an
|
|
229
|
+
hour. Configure custom SMTP (Resend, Postmark, SES) before relying on
|
|
230
|
+
invites. Submitting the same address again re-sends a pending invite.
|
|
231
|
+
|
|
232
|
+
Prefer a route that verifies server-side rather than relying on client JS to
|
|
233
|
+
notice the redirect? `supabaseConfirmRoute` still works exactly as before —
|
|
234
|
+
mount it and point a custom email template's link at
|
|
235
|
+
`{{ .SiteURL }}/guardrail/confirm?token_hash={{ .TokenHash }}&type=email`:
|
|
142
236
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
```html
|
|
152
|
-
<a href="{{ .SiteURL }}/guardrail/confirm?token_hash={{ .TokenHash }}&type=email">Sign in</a>
|
|
153
|
-
```
|
|
154
|
-
The default `{{ .ConfirmationURL }}` uses a PKCE code that only works in
|
|
155
|
-
the browser that requested it — the admin's, not the invitee's.
|
|
156
|
-
4. **Auth → SMTP** — Supabase's built-in mailer allows only a few emails an
|
|
157
|
-
hour. Configure custom SMTP (Resend, Postmark, SES) before relying on
|
|
158
|
-
invites. Submitting the same address again re-sends a pending invite.
|
|
237
|
+
```ts
|
|
238
|
+
// app/guardrail/confirm/route.ts
|
|
239
|
+
export const GET = supabaseConfirmRoute({ url, anonKey });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The default `{{ .ConfirmationURL }}` uses a PKCE code that only works in the
|
|
243
|
+
browser that requested it — the admin's, not the invitee's — so a custom
|
|
244
|
+
template needs the token hash form above either way.
|
|
159
245
|
|
|
160
246
|
**Bootstrapping.** A site with no members has nobody who can add members, and
|
|
161
247
|
there is deliberately no in-app way to grant yourself a role — that would mean
|
|
@@ -378,18 +464,33 @@ const clerkAuth: GRAuthAdapter = {
|
|
|
378
464
|
};
|
|
379
465
|
```
|
|
380
466
|
|
|
467
|
+
`completeAuthRedirect` and `setPassword` are optional too, and go together —
|
|
468
|
+
an adapter with no concept of a redirect-carried session (an OAuth provider
|
|
469
|
+
whose own SDK already handles its callback route, say) omits both, and links
|
|
470
|
+
of that kind pass through untouched. Implement `completeAuthRedirect` to
|
|
471
|
+
support them, returning `needsPassword: true` for a session that has none
|
|
472
|
+
yet; `setPassword` is what the resulting prompt calls.
|
|
473
|
+
|
|
381
474
|
`getSession()` is the sole authorization check for writes. It must validate the
|
|
382
475
|
session against the provider — never trust the presence of a cookie.
|
|
383
476
|
|
|
477
|
+
`listOverrides(siteId, { prefix, limit })` is optional. It's what lets `<GRail>`
|
|
478
|
+
work inside Client Components (see above): one round trip returning the site's rows
|
|
479
|
+
and whether the list was cut off at `limit`. An adapter without it still works
|
|
480
|
+
fully for Server Components — client-rendered fields just show their JSX fallback.
|
|
481
|
+
|
|
384
482
|
`getOverrides(pathKeys, siteId)` is batched by design — it receives every key a
|
|
385
483
|
render pass needs, already deduplicated. Adapters map their own storage naming
|
|
386
484
|
at this boundary; core speaks `pathKey` / `siteId` only.
|
|
387
485
|
|
|
388
486
|
## Performance
|
|
389
487
|
|
|
390
|
-
Fields are resolved through a request-scoped
|
|
391
|
-
|
|
392
|
-
|
|
488
|
+
Fields are resolved through a request-scoped loader. By default it preloads the
|
|
489
|
+
site's content in one query and every `<GRail>` — server or client — reads from
|
|
490
|
+
that, so a request issues a single content query however many fields it renders.
|
|
491
|
+
Anything outside the preload (a `prefetch.prefix` scope, or a site past the row
|
|
492
|
+
ceiling) falls back to a batched lookup by key. Set `GUARDRAIL_DEBUG=1` to log
|
|
493
|
+
what each query resolved.
|
|
393
494
|
|
|
394
495
|
## Known limitations
|
|
395
496
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { anonClient, assertCredentials, sessionClient } from "./client";
|
|
2
|
+
const NEEDS_PASSWORD_TYPES = /* @__PURE__ */ new Set(["invite", "recovery", "magiclink"]);
|
|
2
3
|
function supabaseAuth(creds) {
|
|
3
4
|
assertCredentials(creds);
|
|
4
5
|
return {
|
|
@@ -48,6 +49,55 @@ function supabaseAuth(creds) {
|
|
|
48
49
|
async logout() {
|
|
49
50
|
const client = await sessionClient(creds);
|
|
50
51
|
await client.auth.signOut();
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Whichever of hash/code/tokenHash is present decides the exchange —
|
|
55
|
+
* these correspond to Supabase's three redirect styles (legacy implicit
|
|
56
|
+
* grant, PKCE, and the token_hash style Guardrail's own confirm route
|
|
57
|
+
* already used), so this handles a link regardless of which one a
|
|
58
|
+
* project's email templates happen to produce, including the
|
|
59
|
+
* unconfigured default (implicit grant, hash-based — what an unedited
|
|
60
|
+
* Supabase project actually sends).
|
|
61
|
+
*
|
|
62
|
+
* Goes through sessionClient(), the same cookie-writing client
|
|
63
|
+
* everything else in this adapter uses — setSession/exchangeCodeForSession/
|
|
64
|
+
* verifyOtp all persist the resulting session through its existing
|
|
65
|
+
* getAll/setAll bridge, so this needs no browser-side Supabase client of
|
|
66
|
+
* its own.
|
|
67
|
+
*/
|
|
68
|
+
async completeAuthRedirect({ hash, code, tokenHash, type }) {
|
|
69
|
+
const client = await sessionClient(creds);
|
|
70
|
+
if (hash) {
|
|
71
|
+
const hashParams = new URLSearchParams(hash);
|
|
72
|
+
const access_token = hashParams.get("access_token");
|
|
73
|
+
const refresh_token = hashParams.get("refresh_token");
|
|
74
|
+
const hashType = hashParams.get("type") ?? type;
|
|
75
|
+
if (!access_token || !refresh_token) {
|
|
76
|
+
return { success: false, error: "Redirect link is missing its tokens." };
|
|
77
|
+
}
|
|
78
|
+
const { error } = await client.auth.setSession({ access_token, refresh_token });
|
|
79
|
+
if (error) return { success: false, error: error.message };
|
|
80
|
+
return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(hashType ?? "") };
|
|
81
|
+
}
|
|
82
|
+
if (code) {
|
|
83
|
+
const { error } = await client.auth.exchangeCodeForSession(code);
|
|
84
|
+
if (error) return { success: false, error: error.message };
|
|
85
|
+
return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type ?? "") };
|
|
86
|
+
}
|
|
87
|
+
if (tokenHash && type) {
|
|
88
|
+
const { error } = await client.auth.verifyOtp({
|
|
89
|
+
type,
|
|
90
|
+
token_hash: tokenHash
|
|
91
|
+
});
|
|
92
|
+
if (error) return { success: false, error: error.message };
|
|
93
|
+
return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type) };
|
|
94
|
+
}
|
|
95
|
+
return { success: false, error: "No redirect data to complete." };
|
|
96
|
+
},
|
|
97
|
+
async setPassword(password) {
|
|
98
|
+
const client = await sessionClient(creds);
|
|
99
|
+
const { error } = await client.auth.updateUser({ password });
|
|
100
|
+
return error ? { success: false, error: error.message } : { success: true };
|
|
51
101
|
}
|
|
52
102
|
};
|
|
53
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/supabase/auth.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport type {
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/supabase/auth.ts"],"sourcesContent":["import type { EmailOtpType } from \"@supabase/supabase-js\";\nimport { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport type {\n AuthRedirectParams,\n GRAuthAdapter,\n GRSession,\n} from \"../../core/adapter-types\";\n\n// \"invite\" covers Supabase's own admin-API invite flow; \"recovery\" is\n// forgot-password. \"magiclink\" belongs here too — deliberately, not an\n// oversight — because inviteUser() below sends one (see its own comment for\n// why) rather than an \"invite\"-typed email, so this is the type Guardrail's\n// actual, real invite flow produces. A returning magic-link user who already\n// has a password sees this redundantly, but it's skippable, and the\n// alternative — never prompting the person this feature exists for — isn't\n// an acceptable trade to avoid that.\nconst NEEDS_PASSWORD_TYPES = new Set([\"invite\", \"recovery\", \"magiclink\"]);\n\nexport function supabaseAuth(creds: SupabaseCredentials): GRAuthAdapter {\n assertCredentials(creds);\n\n return {\n async authenticate(email, password) {\n const client = await sessionClient(creds);\n const { data, error } = await client.auth.signInWithPassword({\n email,\n password,\n });\n\n if (error) return { success: false, error: error.message };\n if (!data.user) return { success: false, error: \"User not found.\" };\n\n return {\n success: true,\n session: { userId: data.user.id, email: data.user.email },\n };\n },\n\n /**\n * Uses getUser(), which revalidates the JWT against the Supabase auth\n * server. getSession() only decodes the cookie, so it would trust a forged\n * token.\n */\n async getSession(): Promise<GRSession | null> {\n const client = await sessionClient(creds);\n const { data, error } = await client.auth.getUser();\n if (error || !data.user) return null;\n return { userId: data.user.id, email: data.user.email };\n },\n\n /**\n * Sends a sign-in link that creates the account on first use.\n *\n * Requested through the cookie-free client on purpose. A session client\n * would use PKCE and drop the code verifier into the *admin's* browser,\n * so the link would fail when the invitee opened it anywhere else. The\n * email template must point at the confirm route with {{ .TokenHash }},\n * which verifies server-side and needs no verifier.\n */\n async inviteUser(email, options) {\n const { error } = await anonClient(creds).auth.signInWithOtp({\n email,\n options: {\n shouldCreateUser: true,\n ...(options?.redirectTo ? { emailRedirectTo: options.redirectTo } : {}),\n },\n });\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async logout() {\n const client = await sessionClient(creds);\n await client.auth.signOut();\n },\n\n /**\n * Whichever of hash/code/tokenHash is present decides the exchange —\n * these correspond to Supabase's three redirect styles (legacy implicit\n * grant, PKCE, and the token_hash style Guardrail's own confirm route\n * already used), so this handles a link regardless of which one a\n * project's email templates happen to produce, including the\n * unconfigured default (implicit grant, hash-based — what an unedited\n * Supabase project actually sends).\n *\n * Goes through sessionClient(), the same cookie-writing client\n * everything else in this adapter uses — setSession/exchangeCodeForSession/\n * verifyOtp all persist the resulting session through its existing\n * getAll/setAll bridge, so this needs no browser-side Supabase client of\n * its own.\n */\n async completeAuthRedirect({ hash, code, tokenHash, type }: AuthRedirectParams) {\n const client = await sessionClient(creds);\n\n if (hash) {\n const hashParams = new URLSearchParams(hash);\n const access_token = hashParams.get(\"access_token\");\n const refresh_token = hashParams.get(\"refresh_token\");\n const hashType = hashParams.get(\"type\") ?? type;\n if (!access_token || !refresh_token) {\n return { success: false, error: \"Redirect link is missing its tokens.\" };\n }\n const { error } = await client.auth.setSession({ access_token, refresh_token });\n if (error) return { success: false, error: error.message };\n return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(hashType ?? \"\") };\n }\n\n if (code) {\n const { error } = await client.auth.exchangeCodeForSession(code);\n if (error) return { success: false, error: error.message };\n return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type ?? \"\") };\n }\n\n if (tokenHash && type) {\n const { error } = await client.auth.verifyOtp({\n type: type as EmailOtpType,\n token_hash: tokenHash,\n });\n if (error) return { success: false, error: error.message };\n return { success: true, needsPassword: NEEDS_PASSWORD_TYPES.has(type) };\n }\n\n return { success: false, error: \"No redirect data to complete.\" };\n },\n\n async setPassword(password: string) {\n const client = await sessionClient(creds);\n const { error } = await client.auth.updateUser({ password });\n return error ? { success: false, error: error.message } : { success: true };\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -20,6 +20,17 @@ function supabaseDB(creds) {
|
|
|
20
20
|
}
|
|
21
21
|
return (data ?? []).map(toOverrideRow);
|
|
22
22
|
},
|
|
23
|
+
async listOverrides(siteId, { prefix, limit }) {
|
|
24
|
+
let query = anonClient(creds).schema(GR_SCHEMA).from(GR_TABLE).select("id, path_key, value").eq("site_id", siteId).order("path_key").limit(limit + 1);
|
|
25
|
+
if (prefix?.length) {
|
|
26
|
+
const quote = (p) => `"${p.replace(/["\\]/g, "\\$&")}"`;
|
|
27
|
+
query = query.or(prefix.map((p) => `path_key.like.${quote(`${p}*`)}`).join(","));
|
|
28
|
+
}
|
|
29
|
+
const { data, error } = await query;
|
|
30
|
+
if (error) throw new Error(error.message);
|
|
31
|
+
const rows = (data ?? []).map(toOverrideRow);
|
|
32
|
+
return { rows: rows.slice(0, limit), truncated: rows.length > limit };
|
|
33
|
+
},
|
|
23
34
|
async setOverride(row) {
|
|
24
35
|
const client = await sessionClient(creds);
|
|
25
36
|
const { error } = await client.schema(GR_SCHEMA).from(GR_TABLE).upsert(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/adapters/supabase/db.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport { GR_SCHEMA, GR_TABLE } from \"../../core/defs\";\nimport type { GRDBAdapter, GRStorageRow } from \"../../core/adapter-types\";\nimport type { OverrideRow, OverrideValue } from \"../../core/types\";\n\n/** Shape as stored in Postgres. Column naming stops at this boundary. */\ninterface ContentRow {\n id: string;\n path_key: string;\n value: OverrideValue | string;\n}\n\nfunction toOverrideRow(row: ContentRow): OverrideRow {\n return {\n id: row.id,\n pathKey: row.path_key,\n // Some drivers hand back jsonb columns as strings.\n value:\n typeof row.value === \"string\"\n ? (JSON.parse(row.value) as OverrideValue)\n : row.value,\n };\n}\n\nexport function supabaseDB(creds: SupabaseCredentials): GRDBAdapter {\n assertCredentials(creds);\n\n return {\n async getOverrides(pathKeys, siteId) {\n if (pathKeys.length === 0) return [];\n\n // Anon client keeps the read path cookie-free so routes stay static.\n const { data, error } = await anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .select(\"id, path_key, value\")\n .eq(\"site_id\", siteId)\n .in(\"path_key\", pathKeys);\n\n if (error) {\n console.error(\"Guardrail (Supabase): read failed\", error.message);\n return [];\n }\n return ((data ?? []) as ContentRow[]).map(toOverrideRow);\n },\n\n async setOverride(row: GRStorageRow) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .upsert(\n {\n path_key: row.pathKey,\n site_id: row.siteId,\n type: row.type,\n value: row.value,\n updated_at: new Date().toISOString(),\n },\n { onConflict: \"path_key,site_id\" },\n );\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async deleteOverrides(pathKeyPrefix: string, siteId: string) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .delete()\n .eq(\"site_id\", siteId)\n .like(\"path_key\", `${pathKeyPrefix}%`);\n if (error) {\n console.error(\"Guardrail (Supabase): deleteOverrides failed\", error.message);\n }\n },\n };\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB,qBAAqB;AAE7D,SAAS,WAAW,gBAAgB;AAWpC,SAAS,cAAc,KAA8B;AACnD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA;AAAA,IAEb,OACE,OAAO,IAAI,UAAU,WAChB,KAAK,MAAM,IAAI,KAAK,IACrB,IAAI;AAAA,EACZ;AACF;AAEO,SAAS,WAAW,OAAyC;AAClE,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,aAAa,UAAU,QAAQ;AACnC,UAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAGnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,EAC3C,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,qBAAqB,EAC5B,GAAG,WAAW,MAAM,EACpB,GAAG,YAAY,QAAQ;AAE1B,UAAI,OAAO;AACT,gBAAQ,MAAM,qCAAqC,MAAM,OAAO;AAChE,eAAO,CAAC;AAAA,MACV;AACA,cAAS,QAAQ,CAAC,GAAoB,IAAI,aAAa;AAAA,IACzD;AAAA,IAEA,MAAM,YAAY,KAAmB;AACnC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb;AAAA,QACC;AAAA,UACE,UAAU,IAAI;AAAA,UACd,SAAS,IAAI;AAAA,UACb,MAAM,IAAI;AAAA,UACV,OAAO,IAAI;AAAA,UACX,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC;AAAA,QACA,EAAE,YAAY,mBAAmB;AAAA,MACnC;AACF,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,gBAAgB,eAAuB,QAAgB;AAC3D,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,EACP,GAAG,WAAW,MAAM,EACpB,KAAK,YAAY,GAAG,aAAa,GAAG;AACvC,UAAI,OAAO;AACT,gBAAQ,MAAM,gDAAgD,MAAM,OAAO;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/adapters/supabase/db.ts"],"sourcesContent":["import { anonClient, assertCredentials, sessionClient } from \"./client\";\nimport type { SupabaseCredentials } from \"./client\";\nimport { GR_SCHEMA, GR_TABLE } from \"../../core/defs\";\nimport type { GRDBAdapter, GRStorageRow } from \"../../core/adapter-types\";\nimport type { OverrideRow, OverrideValue } from \"../../core/types\";\n\n/** Shape as stored in Postgres. Column naming stops at this boundary. */\ninterface ContentRow {\n id: string;\n path_key: string;\n value: OverrideValue | string;\n}\n\nfunction toOverrideRow(row: ContentRow): OverrideRow {\n return {\n id: row.id,\n pathKey: row.path_key,\n // Some drivers hand back jsonb columns as strings.\n value:\n typeof row.value === \"string\"\n ? (JSON.parse(row.value) as OverrideValue)\n : row.value,\n };\n}\n\nexport function supabaseDB(creds: SupabaseCredentials): GRDBAdapter {\n assertCredentials(creds);\n\n return {\n async getOverrides(pathKeys, siteId) {\n if (pathKeys.length === 0) return [];\n\n // Anon client keeps the read path cookie-free so routes stay static.\n const { data, error } = await anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .select(\"id, path_key, value\")\n .eq(\"site_id\", siteId)\n .in(\"path_key\", pathKeys);\n\n if (error) {\n console.error(\"Guardrail (Supabase): read failed\", error.message);\n return [];\n }\n return ((data ?? []) as ContentRow[]).map(toOverrideRow);\n },\n\n async listOverrides(siteId, { prefix, limit }) {\n let query = anonClient(creds)\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .select(\"id, path_key, value\")\n .eq(\"site_id\", siteId)\n // Ordered so that truncation, when it happens, is deterministic.\n .order(\"path_key\")\n // One past the ceiling, purely to learn whether anything was cut off.\n .limit(limit + 1);\n\n if (prefix?.length) {\n // Quoted so a prefix containing PostgREST's own separators (comma,\n // parens) can't be read as filter syntax.\n const quote = (p: string) => `\"${p.replace(/[\"\\\\]/g, \"\\\\$&\")}\"`;\n query = query.or(prefix.map((p) => `path_key.like.${quote(`${p}*`)}`).join(\",\"));\n }\n\n const { data, error } = await query;\n // Thrown, not swallowed to []: an empty list here would read as \"this\n // site has no content\" and short-circuit every field's own lookup.\n if (error) throw new Error(error.message);\n\n const rows = ((data ?? []) as ContentRow[]).map(toOverrideRow);\n return { rows: rows.slice(0, limit), truncated: rows.length > limit };\n },\n\n async setOverride(row: GRStorageRow) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .upsert(\n {\n path_key: row.pathKey,\n site_id: row.siteId,\n type: row.type,\n value: row.value,\n updated_at: new Date().toISOString(),\n },\n { onConflict: \"path_key,site_id\" },\n );\n return error\n ? { success: false, error: error.message }\n : { success: true };\n },\n\n async deleteOverrides(pathKeyPrefix: string, siteId: string) {\n const client = await sessionClient(creds);\n const { error } = await client\n .schema(GR_SCHEMA)\n .from(GR_TABLE)\n .delete()\n .eq(\"site_id\", siteId)\n .like(\"path_key\", `${pathKeyPrefix}%`);\n if (error) {\n console.error(\"Guardrail (Supabase): deleteOverrides failed\", error.message);\n }\n },\n };\n}\n"],"mappings":"AAAA,SAAS,YAAY,mBAAmB,qBAAqB;AAE7D,SAAS,WAAW,gBAAgB;AAWpC,SAAS,cAAc,KAA8B;AACnD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA;AAAA,IAEb,OACE,OAAO,IAAI,UAAU,WAChB,KAAK,MAAM,IAAI,KAAK,IACrB,IAAI;AAAA,EACZ;AACF;AAEO,SAAS,WAAW,OAAyC;AAClE,oBAAkB,KAAK;AAEvB,SAAO;AAAA,IACL,MAAM,aAAa,UAAU,QAAQ;AACnC,UAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AAGnC,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK,EAC3C,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,qBAAqB,EAC5B,GAAG,WAAW,MAAM,EACpB,GAAG,YAAY,QAAQ;AAE1B,UAAI,OAAO;AACT,gBAAQ,MAAM,qCAAqC,MAAM,OAAO;AAChE,eAAO,CAAC;AAAA,MACV;AACA,cAAS,QAAQ,CAAC,GAAoB,IAAI,aAAa;AAAA,IACzD;AAAA,IAEA,MAAM,cAAc,QAAQ,EAAE,QAAQ,MAAM,GAAG;AAC7C,UAAI,QAAQ,WAAW,KAAK,EACzB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,qBAAqB,EAC5B,GAAG,WAAW,MAAM,EAEpB,MAAM,UAAU,EAEhB,MAAM,QAAQ,CAAC;AAElB,UAAI,QAAQ,QAAQ;AAGlB,cAAM,QAAQ,CAAC,MAAc,IAAI,EAAE,QAAQ,UAAU,MAAM,CAAC;AAC5D,gBAAQ,MAAM,GAAG,OAAO,IAAI,CAAC,MAAM,iBAAiB,MAAM,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,GAAG,CAAC;AAAA,MACjF;AAEA,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM;AAG9B,UAAI,MAAO,OAAM,IAAI,MAAM,MAAM,OAAO;AAExC,YAAM,QAAS,QAAQ,CAAC,GAAoB,IAAI,aAAa;AAC7D,aAAO,EAAE,MAAM,KAAK,MAAM,GAAG,KAAK,GAAG,WAAW,KAAK,SAAS,MAAM;AAAA,IACtE;AAAA,IAEA,MAAM,YAAY,KAAmB;AACnC,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb;AAAA,QACC;AAAA,UACE,UAAU,IAAI;AAAA,UACd,SAAS,IAAI;AAAA,UACb,MAAM,IAAI;AAAA,UACV,OAAO,IAAI;AAAA,UACX,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC;AAAA,QACA,EAAE,YAAY,mBAAmB;AAAA,MACnC;AACF,aAAO,QACH,EAAE,SAAS,OAAO,OAAO,MAAM,QAAQ,IACvC,EAAE,SAAS,KAAK;AAAA,IACtB;AAAA,IAEA,MAAM,gBAAgB,eAAuB,QAAgB;AAC3D,YAAM,SAAS,MAAM,cAAc,KAAK;AACxC,YAAM,EAAE,MAAM,IAAI,MAAM,OACrB,OAAO,SAAS,EAChB,KAAK,QAAQ,EACb,OAAO,EACP,GAAG,WAAW,MAAM,EACpB,KAAK,YAAY,GAAG,aAAa,GAAG;AACvC,UAAI,OAAO;AACT,gBAAQ,MAAM,gDAAgD,MAAM,OAAO;AAAA,MAC7E;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeFileSync
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
function commandExists(cmd) {
|
|
14
|
+
const res = process.platform === "win32" ? spawnSync("where", [cmd]) : spawnSync("which", [cmd]);
|
|
15
|
+
return res.status === 0;
|
|
16
|
+
}
|
|
17
|
+
function run(cmd, args) {
|
|
18
|
+
const res = spawnSync(cmd, args, { stdio: "inherit" });
|
|
19
|
+
if (res.status !== 0) {
|
|
20
|
+
throw new Error(`${cmd} ${args.join(" ")} exited with code ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function fail(message) {
|
|
24
|
+
console.error(`
|
|
25
|
+
${message}
|
|
26
|
+
`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const USAGE = `Usage:
|
|
30
|
+
guardrail-init-db [--push]
|
|
31
|
+
|
|
32
|
+
Copies Guardrail's migration(s) from this package into ./supabase/migrations/
|
|
33
|
+
(skipping any already present there), runs \`supabase init\` if this project
|
|
34
|
+
has no supabase/config.toml yet, and adds "guardrail" to its exposed schemas
|
|
35
|
+
for local dev.
|
|
36
|
+
|
|
37
|
+
--push Also run \`supabase db push\` once the files are in place. Without it,
|
|
38
|
+
the migration is copied but not applied \u2014 run \`supabase db push\`
|
|
39
|
+
yourself (after \`supabase link\`, if this project isn't linked yet).`;
|
|
40
|
+
function patchExposedSchemas(configPath) {
|
|
41
|
+
const content = readFileSync(configPath, "utf8");
|
|
42
|
+
const patched = content.replace(
|
|
43
|
+
/(\[api\][\s\S]*?schemas\s*=\s*\[)([^\]]*)(\])/,
|
|
44
|
+
(match, pre, inner, post) => {
|
|
45
|
+
if (inner.includes('"guardrail"')) return match;
|
|
46
|
+
const trimmed = inner.trimEnd();
|
|
47
|
+
const sep = trimmed === "" || trimmed.endsWith(",") ? " " : ", ";
|
|
48
|
+
return `${pre}${inner}${sep}"guardrail"${post}`;
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
if (patched === content && !content.includes('"guardrail"')) {
|
|
52
|
+
console.warn(
|
|
53
|
+
'Could not find the [api] schemas setting in supabase/config.toml to patch automatically \u2014 add "guardrail" to it yourself before `supabase start`.'
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
writeFileSync(configPath, patched);
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
62
|
+
console.log(USAGE);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const push = args.includes("--push");
|
|
66
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
const pkgRoot = path.join(__dirname, "..", "..");
|
|
68
|
+
const srcDir = path.join(pkgRoot, "supabase", "migrations");
|
|
69
|
+
if (!existsSync(srcDir)) {
|
|
70
|
+
fail(`No bundled migrations found at ${srcDir} \u2014 is @guard-rail/core installed correctly?`);
|
|
71
|
+
}
|
|
72
|
+
const destDir = path.join(process.cwd(), "supabase", "migrations");
|
|
73
|
+
mkdirSync(destDir, { recursive: true });
|
|
74
|
+
const existing = new Set(readdirSync(destDir));
|
|
75
|
+
const copied = [];
|
|
76
|
+
for (const file of readdirSync(srcDir)) {
|
|
77
|
+
if (existing.has(file)) continue;
|
|
78
|
+
copyFileSync(path.join(srcDir, file), path.join(destDir, file));
|
|
79
|
+
copied.push(file);
|
|
80
|
+
}
|
|
81
|
+
if (copied.length === 0) {
|
|
82
|
+
console.log("supabase/migrations/ is already up to date \u2014 nothing to copy.");
|
|
83
|
+
} else {
|
|
84
|
+
console.log(`Copied ${copied.length} migration${copied.length === 1 ? "" : "s"}:`);
|
|
85
|
+
for (const file of copied) console.log(` supabase/migrations/${file}`);
|
|
86
|
+
}
|
|
87
|
+
if (!commandExists("supabase")) {
|
|
88
|
+
console.log(
|
|
89
|
+
"\nSupabase CLI not found \u2014 apply the migration yourself (paste it into the SQL editor, or install the CLI and run `supabase db push`)."
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const configPath = path.join(process.cwd(), "supabase", "config.toml");
|
|
94
|
+
if (!existsSync(configPath)) {
|
|
95
|
+
try {
|
|
96
|
+
run("supabase", ["init"]);
|
|
97
|
+
} catch {
|
|
98
|
+
console.warn("\n`supabase init` failed \u2014 you can run it yourself.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (existsSync(configPath)) patchExposedSchemas(configPath);
|
|
103
|
+
if (!push) {
|
|
104
|
+
console.log(
|
|
105
|
+
"\nRun `guardrail-init-db --push` (or `supabase db push` yourself) to apply it."
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
run("supabase", ["db", "push"]);
|
|
111
|
+
} catch {
|
|
112
|
+
console.warn(
|
|
113
|
+
"\n`supabase db push` failed \u2014 if this project isn't linked yet, run `supabase link` first, then `supabase db push`."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
main().catch((err) => {
|
|
118
|
+
console.error(err);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=init-db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/init-db.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Copies Guardrail's SQL migration(s) into the current project's own\n * supabase/migrations/, then (optionally) applies them — the step\n * create-guardrail already automates for a brand-new scaffold (see\n * copyMigration()/maybeLinkAndPush() in that package), but has no\n * equivalent for an existing Next.js + Supabase app that just ran\n * `npm install @guard-rail/core`. Until now, postinstall.mjs pointed at\n * doing this by hand: \"Copy\n * node_modules/@guard-rail/core/supabase/migrations/*.sql into your own\n * supabase/migrations/, then `supabase db push`.\"\n *\n * Copies by filename, skipping anything already present in the destination\n * — safe to re-run after upgrading the package: it only ever adds\n * migrations this version ships that a prior run didn't already copy, and\n * never overwrites one that's already there.\n *\n * Deliberately non-interactive (flags only, no prompts), unlike\n * create-guardrail's wizard-style prompts — this is meant to be a single\n * command a developer (or a script) runs once against a project that\n * already exists, not a guided setup flow.\n */\nimport { spawnSync } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n copyFileSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction commandExists(cmd: string): boolean {\n // See create-guardrail's util.ts for why `which`/`where` over `command -v`\n // with shell:true — no shell needed, no Node deprecation warning.\n const res =\n process.platform === \"win32\" ? spawnSync(\"where\", [cmd]) : spawnSync(\"which\", [cmd]);\n return res.status === 0;\n}\n\n/** Throws with the failing command's name — every call site decides whether that's fatal. */\nfunction run(cmd: string, args: string[]): void {\n const res = spawnSync(cmd, args, { stdio: \"inherit\" });\n if (res.status !== 0) {\n throw new Error(`${cmd} ${args.join(\" \")} exited with code ${res.status}`);\n }\n}\n\nfunction fail(message: string): never {\n console.error(`\\n${message}\\n`);\n process.exit(1);\n}\n\nconst USAGE = `Usage:\n guardrail-init-db [--push]\n\nCopies Guardrail's migration(s) from this package into ./supabase/migrations/\n(skipping any already present there), runs \\`supabase init\\` if this project\nhas no supabase/config.toml yet, and adds \"guardrail\" to its exposed schemas\nfor local dev.\n\n--push Also run \\`supabase db push\\` once the files are in place. Without it,\n the migration is copied but not applied — run \\`supabase db push\\`\n yourself (after \\`supabase link\\`, if this project isn't linked yet).`;\n\n/**\n * Idempotent by design (a no-op if \"guardrail\" is already listed) — safe to\n * run on every invocation rather than only the first, including against a\n * config.toml a developer has since hand-edited.\n */\nfunction patchExposedSchemas(configPath: string): void {\n const content = readFileSync(configPath, \"utf8\");\n const patched = content.replace(\n /(\\[api\\][\\s\\S]*?schemas\\s*=\\s*\\[)([^\\]]*)(\\])/,\n (match, pre: string, inner: string, post: string) => {\n if (inner.includes('\"guardrail\"')) return match;\n const trimmed = inner.trimEnd();\n const sep = trimmed === \"\" || trimmed.endsWith(\",\") ? \" \" : \", \";\n return `${pre}${inner}${sep}\"guardrail\"${post}`;\n },\n );\n\n if (patched === content && !content.includes('\"guardrail\"')) {\n console.warn(\n 'Could not find the [api] schemas setting in supabase/config.toml to patch ' +\n 'automatically — add \"guardrail\" to it yourself before `supabase start`.',\n );\n return;\n }\n writeFileSync(configPath, patched);\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n if (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(USAGE);\n return;\n }\n const push = args.includes(\"--push\");\n\n // This file's own location, not the invoking project's node_modules —\n // resolves the migrations this installed package version ships,\n // regardless of how the command was invoked (npx, a package.json script,\n // a workspace link).\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const pkgRoot = path.join(__dirname, \"..\", \"..\");\n const srcDir = path.join(pkgRoot, \"supabase\", \"migrations\");\n if (!existsSync(srcDir)) {\n fail(`No bundled migrations found at ${srcDir} — is @guard-rail/core installed correctly?`);\n }\n\n const destDir = path.join(process.cwd(), \"supabase\", \"migrations\");\n mkdirSync(destDir, { recursive: true });\n const existing = new Set(readdirSync(destDir));\n\n const copied: string[] = [];\n for (const file of readdirSync(srcDir)) {\n if (existing.has(file)) continue;\n copyFileSync(path.join(srcDir, file), path.join(destDir, file));\n copied.push(file);\n }\n\n if (copied.length === 0) {\n console.log(\"supabase/migrations/ is already up to date — nothing to copy.\");\n } else {\n console.log(`Copied ${copied.length} migration${copied.length === 1 ? \"\" : \"s\"}:`);\n for (const file of copied) console.log(` supabase/migrations/${file}`);\n }\n\n if (!commandExists(\"supabase\")) {\n console.log(\n \"\\nSupabase CLI not found — apply the migration yourself (paste it into the SQL \" +\n \"editor, or install the CLI and run `supabase db push`).\",\n );\n return;\n }\n\n const configPath = path.join(process.cwd(), \"supabase\", \"config.toml\");\n if (!existsSync(configPath)) {\n try {\n run(\"supabase\", [\"init\"]);\n } catch {\n console.warn(\"\\n`supabase init` failed — you can run it yourself.\");\n return;\n }\n }\n if (existsSync(configPath)) patchExposedSchemas(configPath);\n\n if (!push) {\n console.log(\n \"\\nRun `guardrail-init-db --push` (or `supabase db push` yourself) to apply it.\",\n );\n return;\n }\n\n try {\n run(\"supabase\", [\"db\", \"push\"]);\n } catch {\n console.warn(\n \"\\n`supabase db push` failed — if this project isn't linked yet, run `supabase \" +\n \"link` first, then `supabase db push`.\",\n );\n }\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
@@ -30,6 +30,30 @@ interface GRDBAdapter {
|
|
|
30
30
|
* row is still removed, just not its old field values.
|
|
31
31
|
*/
|
|
32
32
|
deleteOverrides?: (pathKeyPrefix: string, siteId: string) => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* List a site's rows without naming keys — what makes <GRail> usable inside
|
|
35
|
+
* Client Components, which cannot await a per-field query. One round trip.
|
|
36
|
+
*
|
|
37
|
+
* Optional: an adapter without it still works fully for Server Components;
|
|
38
|
+
* client-rendered fields just show their JSX fallback. `limit` is a ceiling,
|
|
39
|
+
* not a page size — return `truncated: true` if rows beyond it existed, so
|
|
40
|
+
* the caller knows the list is partial. `prefix`, when given, restricts to
|
|
41
|
+
* keys starting with any of those strings.
|
|
42
|
+
*/
|
|
43
|
+
listOverrides?: (siteId: string, opts: {
|
|
44
|
+
prefix?: string[];
|
|
45
|
+
limit: number;
|
|
46
|
+
}) => Promise<{
|
|
47
|
+
rows: OverrideRow[];
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
/** How much content to preload for <GRail>s inside Client Components. */
|
|
52
|
+
interface GRPrefetch {
|
|
53
|
+
/** Only preload keys starting with one of these (e.g. `["home.", "header."]`). */
|
|
54
|
+
prefix?: string[];
|
|
55
|
+
/** Ceiling on preloaded rows. Default 500. */
|
|
56
|
+
limit?: number;
|
|
33
57
|
}
|
|
34
58
|
/** Identity of a signed-in editor, as verified by the auth adapter. */
|
|
35
59
|
interface GRSession {
|
|
@@ -73,6 +97,48 @@ interface GRAuthAdapter {
|
|
|
73
97
|
*/
|
|
74
98
|
getSession: () => Promise<GRSession | null>;
|
|
75
99
|
logout: () => Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Completes a session that arrived via a redirect link — an invite, magic
|
|
102
|
+
* link, or password-recovery email — rather than the normal sign-in form.
|
|
103
|
+
* Optional: an adapter with no such concept (an OAuth provider whose own
|
|
104
|
+
* SDK already handles its callback route, say) simply omits it, and links
|
|
105
|
+
* of this kind pass through Guardrail untouched.
|
|
106
|
+
*
|
|
107
|
+
* Whatever the client found in the URL is handed through as-is — which of
|
|
108
|
+
* `hash`/`code`/`tokenHash` is present depends on the provider's own
|
|
109
|
+
* redirect style, not a choice Guardrail's core makes. `needsPassword` in
|
|
110
|
+
* the result tells the caller to prompt for one: an invited or
|
|
111
|
+
* password-recovering person has a valid session but no password yet.
|
|
112
|
+
*/
|
|
113
|
+
completeAuthRedirect?: (params: AuthRedirectParams) => Promise<{
|
|
114
|
+
success: boolean;
|
|
115
|
+
needsPassword?: boolean;
|
|
116
|
+
error?: string;
|
|
117
|
+
}>;
|
|
118
|
+
/**
|
|
119
|
+
* Sets the current session's password — the other half of
|
|
120
|
+
* `completeAuthRedirect`'s `needsPassword`. Optional for the same reason
|
|
121
|
+
* `authenticate` is: only meaningful for a password-based adapter.
|
|
122
|
+
*/
|
|
123
|
+
setPassword?: (password: string) => Promise<{
|
|
124
|
+
success: boolean;
|
|
125
|
+
error?: string;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Raw redirect data as the browser found it — see completeAuthRedirect.
|
|
130
|
+
* Hash-based tokens never reach the server on their own; the browser has to
|
|
131
|
+
* read them and hand them here.
|
|
132
|
+
*/
|
|
133
|
+
interface AuthRedirectParams {
|
|
134
|
+
/** window.location.hash, minus its leading '#' (the implicit-grant style). */
|
|
135
|
+
hash?: string;
|
|
136
|
+
/** The `code` query param (the PKCE style). */
|
|
137
|
+
code?: string;
|
|
138
|
+
/** The `token_hash` query param (Guardrail's own documented confirm-route style, detected here too so that route stays opt-in rather than required). */
|
|
139
|
+
tokenHash?: string;
|
|
140
|
+
/** Accompanies `code` or `tokenHash` — e.g. "invite", "recovery", "magiclink". */
|
|
141
|
+
type?: string;
|
|
76
142
|
}
|
|
77
143
|
type AuthResult = {
|
|
78
144
|
success: true;
|
|
@@ -207,6 +273,16 @@ interface GRConfig {
|
|
|
207
273
|
* an already-resolved element, not a function.
|
|
208
274
|
*/
|
|
209
275
|
toolbarExtra?: ReactNode;
|
|
276
|
+
/**
|
|
277
|
+
* Content preloaded for <GRail>s inside Client Components. On by default
|
|
278
|
+
* with a row ceiling and no scoping: zero config, and one query per request
|
|
279
|
+
* total, since server-rendered fields read from the same preload instead of
|
|
280
|
+
* issuing their own. Scope it with `prefix` if a site outgrows the ceiling
|
|
281
|
+
* (a dev-mode warning says when), or pass `false` to turn it off — client
|
|
282
|
+
* fields then render their JSX fallback and server fields query per batch as
|
|
283
|
+
* before.
|
|
284
|
+
*/
|
|
285
|
+
prefetch?: false | GRPrefetch;
|
|
210
286
|
}
|
|
211
287
|
|
|
212
|
-
export type { AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
|
288
|
+
export type { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
package/dist/core/config.d.ts
CHANGED
|
@@ -21,7 +21,13 @@ declare function getMembers(): GRMembershipAdapter;
|
|
|
21
21
|
declare function getStorage(): GRStorageAdapter | undefined;
|
|
22
22
|
/** Throws with a clear message if no page on this site uses <GRepeater>. */
|
|
23
23
|
declare function getRepeaters(): GRRepeaterAdapter;
|
|
24
|
+
declare const DEFAULT_PREFETCH_LIMIT = 500;
|
|
25
|
+
/** Normalized preload settings, or null when preloading is off. */
|
|
26
|
+
declare function getPrefetch(): {
|
|
27
|
+
prefix?: string[];
|
|
28
|
+
limit: number;
|
|
29
|
+
} | null;
|
|
24
30
|
declare function getInviteHandler(): GRConfig["onInvite"];
|
|
25
31
|
declare function resolveSiteId(): Promise<string>;
|
|
26
32
|
|
|
27
|
-
export { configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getRepeaters, getStorage, resolveSiteId };
|
|
33
|
+
export { DEFAULT_PREFETCH_LIMIT, configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getPrefetch, getRepeaters, getStorage, resolveSiteId };
|
package/dist/core/config.js
CHANGED
|
@@ -26,6 +26,15 @@ function getStorage() {
|
|
|
26
26
|
function getRepeaters() {
|
|
27
27
|
return required("repeaters");
|
|
28
28
|
}
|
|
29
|
+
const DEFAULT_PREFETCH_LIMIT = 500;
|
|
30
|
+
function getPrefetch() {
|
|
31
|
+
const p = _config?.prefetch;
|
|
32
|
+
if (p === false) return null;
|
|
33
|
+
return {
|
|
34
|
+
...p?.prefix?.length ? { prefix: p.prefix } : {},
|
|
35
|
+
limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT
|
|
36
|
+
};
|
|
37
|
+
}
|
|
29
38
|
function getInviteHandler() {
|
|
30
39
|
return _config?.onInvite;
|
|
31
40
|
}
|
|
@@ -34,11 +43,13 @@ async function resolveSiteId() {
|
|
|
34
43
|
return typeof sid === "function" ? await sid() : sid || "default";
|
|
35
44
|
}
|
|
36
45
|
export {
|
|
46
|
+
DEFAULT_PREFETCH_LIMIT,
|
|
37
47
|
configureGuardrail,
|
|
38
48
|
getAuth,
|
|
39
49
|
getDB,
|
|
40
50
|
getInviteHandler,
|
|
41
51
|
getMembers,
|
|
52
|
+
getPrefetch,
|
|
42
53
|
getRepeaters,
|
|
43
54
|
getStorage,
|
|
44
55
|
resolveSiteId
|