create-smoodly-app 0.0.1

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.
@@ -0,0 +1,25 @@
1
+ import { f, smoodly } from "smoodly";
2
+
3
+ export const people = smoodly.collection({
4
+ name: "people",
5
+ title: "People",
6
+ titleField: "name",
7
+ fields: {
8
+ name: f.text(),
9
+ role: f.text().optional(),
10
+ },
11
+ });
12
+
13
+ export const articles = smoodly.collection({
14
+ name: "articles",
15
+ title: "Articles",
16
+ titleField: "title",
17
+ fields: {
18
+ title: f.text(),
19
+ slug: f.slug({ from: "title" }),
20
+ excerpt: f.text().optional(),
21
+ author: f.ref(() => people),
22
+ },
23
+ });
24
+
25
+ export const collections = [people, articles];
@@ -0,0 +1,31 @@
1
+ import { f, smoodly, type Props } from "smoodly";
2
+ import { articles } from "../../collections";
3
+
4
+ export const articleListSchema = smoodly.schema.section({
5
+ name: "articleList",
6
+ title: "Article list",
7
+ fields: {
8
+ title: f.text(),
9
+ // .resolve("author") — hydrate each article AND follow its author ref
10
+ // one level (article → person), all in resolveTree's batched pass.
11
+ items: f.refList(() => articles).resolve("author"),
12
+ },
13
+ });
14
+
15
+ function ArticleListView({ title, items, styles }: Props<typeof articleListSchema>) {
16
+ return (
17
+ <smoodly.Section styles={styles}>
18
+ <h2>{title}</h2>
19
+ <ul>
20
+ {/* an item demoted/deleted after publish resolves to null — skip it */}
21
+ {items.filter(Boolean).map((article, i) => (
22
+ <li key={i}>
23
+ <strong>{article.title}</strong> — {article.author?.name ?? "unknown author"}
24
+ </li>
25
+ ))}
26
+ </ul>
27
+ </smoodly.Section>
28
+ );
29
+ }
30
+
31
+ export const ArticleList = smoodly.section(articleListSchema, ArticleListView);
@@ -0,0 +1,35 @@
1
+ import { f, smoodly, type Props } from "smoodly";
2
+
3
+ // The locked-zone section: every page renders exactly one of these in its
4
+ // `prefooter` zone. Editors change the words, never the structure — no
5
+ // add, remove, move or duplicate. The editor materializes the node from
6
+ // `sample` the first time it opens a page whose prefooter is empty.
7
+ export const ctaBannerSchema = smoodly.schema.section({
8
+ name: "ctaBanner",
9
+ title: "CTA banner",
10
+ description: "The closing call to action under every page.",
11
+ fields: {
12
+ heading: f.text(),
13
+ buttonLabel: f.text(),
14
+ buttonHref: f.text(),
15
+ },
16
+ styles: {
17
+ tone: f.select(["light", "dark"]).default("dark"),
18
+ },
19
+ sample: {
20
+ heading: "Ready to try Smoodly?",
21
+ buttonLabel: "Read the docs",
22
+ buttonHref: "/docs/intro",
23
+ },
24
+ });
25
+
26
+ function CtaBannerView({ heading, buttonLabel, buttonHref, styles }: Props<typeof ctaBannerSchema>) {
27
+ return (
28
+ <smoodly.Section styles={styles}>
29
+ <h2>{heading}</h2>
30
+ <a href={buttonHref}>{buttonLabel}</a>
31
+ </smoodly.Section>
32
+ );
33
+ }
34
+
35
+ export const CtaBanner = smoodly.section(ctaBannerSchema, CtaBannerView);
@@ -0,0 +1,24 @@
1
+ import { f, smoodly, type Props } from "smoodly";
2
+
3
+ export const heroSchema = smoodly.schema.section({
4
+ name: "hero",
5
+ title: "Hero",
6
+ fields: {
7
+ heading: f.text(),
8
+ kicker: f.text().optional(),
9
+ },
10
+ styles: {
11
+ tone: f.select(["light", "dark"]).default("light"),
12
+ },
13
+ });
14
+
15
+ function HeroView({ heading, kicker, styles }: Props<typeof heroSchema>) {
16
+ return (
17
+ <smoodly.Section styles={styles}>
18
+ {kicker ? <p>{kicker}</p> : null}
19
+ <h1>{heading}</h1>
20
+ </smoodly.Section>
21
+ );
22
+ }
23
+
24
+ export const Hero = smoodly.section(heroSchema, HeroView);
@@ -0,0 +1,6 @@
1
+ import { createSmoodlyAdmin } from "smoodly/admin/next";
2
+ import { config } from "../../smoodly.config";
3
+ import { publicAuth } from "../smoodly";
4
+ import { smoodlyOp } from "./ops";
5
+
6
+ export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth });
@@ -0,0 +1,64 @@
1
+ // Server-only glue: the client and both stores, constructed lazily so
2
+ // `next build` doesn't need env vars at import time. The env shape is
3
+ // the package's (SMOODLY_*); whether the secret is a service-role key or
4
+ // a cloud write key is invisible here.
5
+ import { createSmoodlyClient, resolveTree, smoodlyEnv, SupabaseEntryStore, SupabasePageStore, type PageTree } from "smoodly";
6
+ import { supabaseAdminAuth, type AdminAuth, type AuthConfig } from "smoodly/admin/next";
7
+ import { createPageRenderer } from "smoodly/next";
8
+ import type { SupabaseClient } from "@supabase/supabase-js";
9
+ import { collections } from "../../collections";
10
+ import { sections } from "../../sections";
11
+
12
+ let cached: { db: SupabaseClient; pages: SupabasePageStore; entries: SupabaseEntryStore; auth: AdminAuth } | null = null;
13
+
14
+ function bound() {
15
+ if (cached) return cached;
16
+ const env = smoodlyEnv();
17
+ const db = createSmoodlyClient(env);
18
+ const entries = new SupabaseEntryStore(db, collections);
19
+ const pages = new SupabasePageStore(db, { sections, entries });
20
+ const auth = supabaseAdminAuth({ authUrl: env.authUrl, authKey: env.authKey, db });
21
+ cached = { db, pages, entries, auth };
22
+ return cached;
23
+ }
24
+
25
+ export function stores() {
26
+ const { pages, entries } = bound();
27
+ return { pages, entries };
28
+ }
29
+
30
+ /** The server-side client (seed script, one-off admin tasks). */
31
+ export const client = () => bound().db;
32
+
33
+ /** The admin gate: session verified against the auth project, editors row read with the write key. */
34
+ export const adminAuth = () => bound().auth;
35
+
36
+ /** What the browser needs to sign in — public values only. */
37
+ export function publicAuth(): AuthConfig {
38
+ const env = smoodlyEnv();
39
+ return { url: env.authUrl, key: env.authKey };
40
+ }
41
+
42
+ export const pageTag = (slug: string) => `page:${slug}`;
43
+
44
+ /** One batched pass hydrates every ref in the tree (smoodly's resolveTree);
45
+ * the store's getMany IS the EntryFetcher. The published route passes
46
+ * { status: "published" } so an entry demoted after publish resolves to
47
+ * null on the next revalidation instead of leaking draft fields (OQ #16);
48
+ * the draft-preview path reads unfiltered. */
49
+ export function resolvedTree(tree: PageTree, options?: { status?: "published" }): Promise<PageTree> {
50
+ return resolveTree(tree, {
51
+ sections,
52
+ collections,
53
+ fetch: (collection, ids) => stores().entries.getMany(collection, ids, options),
54
+ });
55
+ }
56
+
57
+ /** The site-route seam: draft branch, tag-cached published read, ref
58
+ * resolution, 404 and editor bridge, bound once for the whole app. */
59
+ export const { renderSmoodlyPage, smoodlyMetadata } = createPageRenderer({
60
+ stores,
61
+ sections,
62
+ collections,
63
+ tag: pageTag,
64
+ });
@@ -0,0 +1,11 @@
1
+ "use server";
2
+ // The app's entire write surface for the admin: ONE exported action,
3
+ // gated by the editors table. ('use server' modules may only export
4
+ // async functions — hence the shape.)
5
+ import { createAdminOpHandler, type AdminOpCall } from "smoodly/admin/next";
6
+ import { config } from "../../smoodly.config";
7
+ import { adminAuth, stores } from "../smoodly";
8
+
9
+ export async function smoodlyOp(call: AdminOpCall) {
10
+ return createAdminOpHandler({ config, ...stores(), auth: adminAuth() })(call);
11
+ }
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const config: NextConfig = {};
4
+
5
+ export default config;
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "smoodly-site",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "next": "^16.3.4",
13
+ "react": "^19",
14
+ "react-dom": "^19",
15
+ "@supabase/supabase-js": "^2.113.0",
16
+ "smoodly": "0.0.1"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^26.4.1",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19.2.5",
22
+ "typescript": "^5"
23
+ }
24
+ }
@@ -0,0 +1,8 @@
1
+ // The registry arrays live in their own modules so lib/smoodly can import
2
+ // them WITHOUT importing smoodly.config.ts — the config imports the page
3
+ // registrations out of app/, and those route files import lib/smoodly.
4
+ import { Hero } from "./components/sections/Hero";
5
+ import { ArticleList } from "./components/sections/ArticleList";
6
+ import { CtaBanner } from "./components/sections/CtaBanner";
7
+
8
+ export const sections = [Hero, ArticleList, CtaBanner];
@@ -0,0 +1,19 @@
1
+ import { defineConfig, f } from "smoodly";
2
+ import { collections } from "./collections";
3
+ import { sections } from "./sections";
4
+ import { HomePage } from "./app/page";
5
+ import { StandardPage } from "./app/[slug]/page";
6
+
7
+ export { collections, sections };
8
+
9
+ export const config = defineConfig({
10
+ registry: {
11
+ sections,
12
+ collections,
13
+ pages: [HomePage, StandardPage],
14
+ },
15
+ locales: { default: "en", supported: ["en"] },
16
+ styles: {
17
+ sections: { spacing: f.select(["sm", "md", "lg"]).default("md") },
18
+ },
19
+ });
@@ -0,0 +1,338 @@
1
+ -- The space a request acts in: the key's claim in Smoodly Cloud, the
2
+ -- default space in self-host (service role, anon key). Policies, the
3
+ -- insert trigger and the reorder RPC all read this ONE function.
4
+ create or replace function "smoodly_current_space"() returns uuid
5
+ language sql stable as $$
6
+ select coalesce((auth.jwt() ->> 'space_id')::uuid, '00000000-0000-0000-0000-000000000000');
7
+ $$;
8
+
9
+ create table if not exists "pages" (
10
+ "id" uuid primary key default gen_random_uuid(),
11
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
12
+ "slug" text not null,
13
+ "template" text not null,
14
+ "status" text not null default 'draft',
15
+ "sort" numeric,
16
+ "locales" text[],
17
+ "draft_version_id" uuid,
18
+ "published_version_id" uuid,
19
+ "created_at" timestamptz not null default now(),
20
+ "updated_at" timestamptz not null default now(),
21
+ unique ("space_id", "slug")
22
+ );
23
+
24
+ create table if not exists "page_versions" (
25
+ "id" uuid primary key default gen_random_uuid(),
26
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
27
+ "page_id" uuid not null references "pages" ("id") on delete cascade,
28
+ "tree" jsonb not null,
29
+ "created_by" text,
30
+ "created_at" timestamptz not null default now()
31
+ );
32
+
33
+ create table if not exists "saved_sections" (
34
+ "id" uuid primary key default gen_random_uuid(),
35
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
36
+ "title" text not null,
37
+ "fragment" jsonb not null,
38
+ "created_at" timestamptz not null default now()
39
+ );
40
+
41
+ create table if not exists "global_sections" (
42
+ "id" uuid primary key default gen_random_uuid(),
43
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
44
+ "type" text not null,
45
+ "content" jsonb not null,
46
+ "updated_at" timestamptz not null default now()
47
+ );
48
+
49
+ create table if not exists "entries" (
50
+ "id" uuid primary key default gen_random_uuid(),
51
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
52
+ "collection" text not null,
53
+ "fields" jsonb not null default '{}'::jsonb,
54
+ "i18n" jsonb,
55
+ "status" text not null default 'draft',
56
+ "sort" numeric,
57
+ "locales" text[],
58
+ "created_at" timestamptz not null default now(),
59
+ "updated_at" timestamptz not null default now()
60
+ );
61
+
62
+ create unique index if not exists "entries_slug_key"
63
+ on "entries" ("space_id", "collection", ("fields"->>'slug'));
64
+
65
+ create index if not exists "entries_collection_status_idx"
66
+ on "entries" ("space_id", "collection", "status");
67
+
68
+ create index if not exists "entries_collection_sort_idx"
69
+ on "entries" ("space_id", "collection", "sort");
70
+
71
+ -- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
72
+ -- statement. Deliberately does not touch updated_at — a drag must not
73
+ -- make the whole collection look freshly edited. Scoped to the current
74
+ -- space by the same function the policies use.
75
+ create or replace function "smoodly_reorder"(
76
+ p_collection text,
77
+ p_ids uuid[]
78
+ ) returns void language sql as $$
79
+ update "entries" e set "sort" = u.ord
80
+ from unnest(p_ids) with ordinality as u(id, ord)
81
+ where e."id" = u.id
82
+ and e."collection" = p_collection
83
+ and e."space_id" = "smoodly_current_space"();
84
+ $$;
85
+
86
+ create table if not exists "refs" (
87
+ "source_kind" text not null,
88
+ "source_id" uuid not null,
89
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
90
+ "source_field" text not null,
91
+ "target_collection" text not null,
92
+ "target_id" uuid not null,
93
+ primary key ("source_kind", "source_id", "source_field", "target_id")
94
+ );
95
+
96
+ -- RLS on every core table. The service role bypasses it (self-host
97
+ -- adapters untouched). The policy set that follows (space layer) admits
98
+ -- a key to its own space: everything for a write key, published rows for
99
+ -- a read key or the anon key (default space). Drafts stay unreachable
100
+ -- to anything but a write key. The generated per-collection views
101
+ -- (collectionSQL) run with security_invoker = true, so they are subject
102
+ -- to this same smoodly_read policy rather than their owner's privileges.
103
+ alter table "pages" enable row level security;
104
+ alter table "page_versions" enable row level security;
105
+ alter table "saved_sections" enable row level security;
106
+ alter table "global_sections" enable row level security;
107
+ alter table "entries" enable row level security;
108
+ alter table "refs" enable row level security;
109
+
110
+ -- ── Cloud state (spec 2026-09-04 §1): three ordinary tables, shipped
111
+ -- for everyone with ONE definition. Self-host fills them by hand or
112
+ -- leaves them empty; smoodly.io writes rows with the service role.
113
+ -- There is never cloud-only SQL on a content database.
114
+ create table if not exists "editors" (
115
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
116
+ "user_id" uuid not null,
117
+ "role" text not null default 'editor',
118
+ primary key ("space_id", "user_id")
119
+ );
120
+
121
+ create table if not exists "space_keys" (
122
+ "id" uuid primary key,
123
+ "space_id" uuid not null,
124
+ "kind" text not null check ("kind" in ('read', 'write')),
125
+ "revoked_at" timestamptz
126
+ );
127
+
128
+ create table if not exists "space_caps" (
129
+ "space_id" uuid primary key,
130
+ "row_cap" integer not null
131
+ );
132
+
133
+ alter table "editors" enable row level security;
134
+ alter table "space_keys" enable row level security;
135
+ alter table "space_caps" enable row level security;
136
+
137
+ -- ── Claim readers. The two lookups run as definer so they can read the
138
+ -- service-role-only tables from inside a policy; search_path is pinned.
139
+ create or replace function "smoodly_key_kind"() returns text
140
+ language sql stable as $$
141
+ select auth.jwt() ->> 'kind';
142
+ $$;
143
+
144
+ create or replace function "smoodly_space_key_revoked"() returns boolean
145
+ language sql stable security definer set search_path = public as $$
146
+ select coalesce(
147
+ (select k."revoked_at" is not null from "space_keys" k
148
+ where k."id" = (auth.jwt() ->> 'key_id')::uuid),
149
+ false);
150
+ $$;
151
+
152
+ create or replace function "smoodly_space_row_cap"() returns integer
153
+ language sql stable security definer set search_path = public as $$
154
+ select c."row_cap" from "space_caps" c where c."space_id" = "smoodly_current_space"();
155
+ $$;
156
+
157
+ -- ── Triggers. A key's claim wins over whatever the insert said, so an
158
+ -- adapter never has to know its space and cannot write into another.
159
+ -- No claim (service role, anon) leaves the given or default value —
160
+ -- that is how smoodly.io writes cloud-state rows for a chosen space.
161
+ create or replace function "smoodly_set_space"() returns trigger
162
+ language plpgsql as $$
163
+ begin
164
+ if auth.jwt() ->> 'space_id' is not null then
165
+ new."space_id" := (auth.jwt() ->> 'space_id')::uuid;
166
+ end if;
167
+ return new;
168
+ end;
169
+ $$;
170
+
171
+ -- Per table, per space. No space_caps row = no cap (self-host). Trigger
172
+ -- a_ (smoodly_set_space) runs first, so new.space_id already equals the
173
+ -- current space for a keyed insert; smoodly_space_row_cap() reads the
174
+ -- current space itself, so this never needs the row's space_id as an
175
+ -- argument (which would let one space's insert probe another's cap).
176
+ -- A service-role insert that names another space explicitly is a
177
+ -- different case: there is no JWT claim, so smoodly_current_space()
178
+ -- still resolves to the default space, and the cap read is the default
179
+ -- space's (none) — the service role is never capped by another space's row.
180
+ -- search_path is pinned empty, so every reference below is schema-qualified.
181
+ create or replace function "smoodly_enforce_row_cap"() returns trigger
182
+ language plpgsql set search_path = '' as $$
183
+ declare
184
+ cap integer;
185
+ n bigint;
186
+ begin
187
+ cap := public."smoodly_space_row_cap"();
188
+ if cap is null then
189
+ return new;
190
+ end if;
191
+ -- Advisory only (free-tier abuse guard), not a hard invariant: two
192
+ -- concurrent inserts can both observe n = cap - 1 and both pass.
193
+ execute format('select count(*) from %I.%I where "space_id" = $1', tg_table_schema, tg_table_name)
194
+ into n using new."space_id";
195
+ if n >= cap then
196
+ raise exception 'smoodly: row cap (%) reached for this space in %', cap, tg_table_name
197
+ using errcode = 'check_violation';
198
+ end if;
199
+ return new;
200
+ end;
201
+ $$;
202
+
203
+ create index if not exists "page_versions_space_idx" on "page_versions" ("space_id");
204
+
205
+ create index if not exists "saved_sections_space_idx" on "saved_sections" ("space_id");
206
+
207
+ create index if not exists "global_sections_space_idx" on "global_sections" ("space_id");
208
+
209
+ create index if not exists "refs_space_idx" on "refs" ("space_id");
210
+
211
+ drop trigger if exists "smoodly_a_set_space" on "pages";
212
+ create trigger "smoodly_a_set_space" before insert on "pages"
213
+ for each row execute function "smoodly_set_space"();
214
+
215
+ drop trigger if exists "smoodly_a_set_space" on "page_versions";
216
+ create trigger "smoodly_a_set_space" before insert on "page_versions"
217
+ for each row execute function "smoodly_set_space"();
218
+
219
+ drop trigger if exists "smoodly_a_set_space" on "saved_sections";
220
+ create trigger "smoodly_a_set_space" before insert on "saved_sections"
221
+ for each row execute function "smoodly_set_space"();
222
+
223
+ drop trigger if exists "smoodly_a_set_space" on "global_sections";
224
+ create trigger "smoodly_a_set_space" before insert on "global_sections"
225
+ for each row execute function "smoodly_set_space"();
226
+
227
+ drop trigger if exists "smoodly_a_set_space" on "entries";
228
+ create trigger "smoodly_a_set_space" before insert on "entries"
229
+ for each row execute function "smoodly_set_space"();
230
+
231
+ drop trigger if exists "smoodly_a_set_space" on "refs";
232
+ create trigger "smoodly_a_set_space" before insert on "refs"
233
+ for each row execute function "smoodly_set_space"();
234
+
235
+ drop trigger if exists "smoodly_a_set_space" on "editors";
236
+ create trigger "smoodly_a_set_space" before insert on "editors"
237
+ for each row execute function "smoodly_set_space"();
238
+
239
+ drop trigger if exists "smoodly_b_row_cap" on "pages";
240
+ create trigger "smoodly_b_row_cap" before insert on "pages"
241
+ for each row execute function "smoodly_enforce_row_cap"();
242
+
243
+ drop trigger if exists "smoodly_b_row_cap" on "page_versions";
244
+ create trigger "smoodly_b_row_cap" before insert on "page_versions"
245
+ for each row execute function "smoodly_enforce_row_cap"();
246
+
247
+ drop trigger if exists "smoodly_b_row_cap" on "saved_sections";
248
+ create trigger "smoodly_b_row_cap" before insert on "saved_sections"
249
+ for each row execute function "smoodly_enforce_row_cap"();
250
+
251
+ drop trigger if exists "smoodly_b_row_cap" on "global_sections";
252
+ create trigger "smoodly_b_row_cap" before insert on "global_sections"
253
+ for each row execute function "smoodly_enforce_row_cap"();
254
+
255
+ drop trigger if exists "smoodly_b_row_cap" on "entries";
256
+ create trigger "smoodly_b_row_cap" before insert on "entries"
257
+ for each row execute function "smoodly_enforce_row_cap"();
258
+
259
+ drop trigger if exists "smoodly_b_row_cap" on "refs";
260
+ create trigger "smoodly_b_row_cap" before insert on "refs"
261
+ for each row execute function "smoodly_enforce_row_cap"();
262
+
263
+ drop policy if exists "smoodly_write" on "pages";
264
+ create policy "smoodly_write" on "pages" for all
265
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
266
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
267
+
268
+ drop policy if exists "smoodly_read" on "pages";
269
+ create policy "smoodly_read" on "pages" for select
270
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
271
+
272
+ drop policy if exists "smoodly_write" on "page_versions";
273
+ create policy "smoodly_write" on "page_versions" for all
274
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
275
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
276
+
277
+ drop policy if exists "smoodly_read" on "page_versions";
278
+ create policy "smoodly_read" on "page_versions" for select
279
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "pages" p where p."published_version_id" = "page_versions"."id"));
280
+
281
+ drop policy if exists "smoodly_write" on "saved_sections";
282
+ create policy "smoodly_write" on "saved_sections" for all
283
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
284
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
285
+
286
+ drop policy if exists "smoodly_write" on "global_sections";
287
+ create policy "smoodly_write" on "global_sections" for all
288
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
289
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
290
+
291
+ drop policy if exists "smoodly_read" on "global_sections";
292
+ create policy "smoodly_read" on "global_sections" for select
293
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()));
294
+
295
+ drop policy if exists "smoodly_write" on "entries";
296
+ create policy "smoodly_write" on "entries" for all
297
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
298
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
299
+
300
+ drop policy if exists "smoodly_read" on "entries";
301
+ create policy "smoodly_read" on "entries" for select
302
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
303
+
304
+ drop policy if exists "smoodly_write" on "refs";
305
+ create policy "smoodly_write" on "refs" for all
306
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
307
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
308
+
309
+ drop policy if exists "smoodly_write" on "editors";
310
+ create policy "smoodly_write" on "editors" for all
311
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
312
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
313
+
314
+ create or replace view "people" with (security_invoker = true) as
315
+ select
316
+ "id",
317
+ "sort",
318
+ "locales",
319
+ "created_at",
320
+ "updated_at",
321
+ "fields"->>'name' as "name",
322
+ "fields"->>'role' as "role"
323
+ from "entries"
324
+ where "collection" = 'people' and "status" = 'published';
325
+
326
+ create or replace view "articles" with (security_invoker = true) as
327
+ select
328
+ "id",
329
+ "sort",
330
+ "locales",
331
+ "created_at",
332
+ "updated_at",
333
+ "fields"->>'title' as "title",
334
+ "fields"->>'slug' as "slug",
335
+ "fields"->>'excerpt' as "excerpt",
336
+ "fields"->>'author' as "author"
337
+ from "entries"
338
+ where "collection" = 'articles' and "status" = 'published';
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
9
+ "allowJs": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "module": "esnext",
15
+ "moduleResolution": "bundler",
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "jsx": "react-jsx",
19
+ "incremental": true,
20
+ "plugins": [
21
+ {
22
+ "name": "next"
23
+ }
24
+ ]
25
+ },
26
+ "include": [
27
+ "next-env.d.ts",
28
+ "**/*.ts",
29
+ "**/*.tsx",
30
+ ".next/types/**/*.ts",
31
+ ".next/dev/types/**/*.ts"
32
+ ],
33
+ "exclude": [
34
+ "node_modules"
35
+ ]
36
+ }