create-smoodly-app 0.0.4 → 0.0.6

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.
Files changed (52) hide show
  1. package/README.md +64 -10
  2. package/dist/index.js +66 -9
  3. package/package.json +1 -1
  4. package/templates/next/README.md +6 -5
  5. package/templates/next/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -1
  6. package/templates/next/app/(smoodly)/admin/layout.tsx +1 -1
  7. package/templates/next/app/[...path]/page.tsx +15 -0
  8. package/templates/next/app/page.tsx +5 -42
  9. package/templates/next/components/pages/Article.tsx +15 -0
  10. package/templates/next/components/pages/Home.tsx +35 -0
  11. package/templates/next/components/pages/Page.tsx +40 -0
  12. package/templates/next/components/sections/ArticleList.tsx +1 -1
  13. package/templates/next/package.json +2 -2
  14. package/templates/next/smoodly/admin.tsx +6 -0
  15. package/templates/next/smoodly/collections.ts +26 -0
  16. package/templates/next/smoodly/config.ts +27 -0
  17. package/templates/next/smoodly/ops.ts +11 -0
  18. package/templates/next/smoodly/sections.ts +8 -0
  19. package/templates/next/smoodly/server.ts +72 -0
  20. package/templates/next/supabase/migrations/00000000000000_smoodly.sql +92 -5
  21. package/templates/next/tsconfig.json +5 -0
  22. package/templates/next-intl/README.md +40 -0
  23. package/templates/next-intl/_gitignore +5 -0
  24. package/templates/next-intl/app/(smoodly)/admin/[[...segments]]/page.tsx +1 -0
  25. package/templates/next-intl/app/(smoodly)/admin/layout.tsx +15 -0
  26. package/templates/next-intl/app/[locale]/[...path]/page.tsx +18 -0
  27. package/templates/next-intl/app/[locale]/layout.tsx +21 -0
  28. package/templates/next-intl/app/[locale]/page.tsx +15 -0
  29. package/templates/next-intl/components/pages/Article.tsx +15 -0
  30. package/templates/next-intl/components/pages/Home.tsx +35 -0
  31. package/templates/next-intl/components/pages/Page.tsx +40 -0
  32. package/templates/next-intl/components/sections/ArticleList.tsx +31 -0
  33. package/templates/next-intl/components/sections/CtaBanner.tsx +35 -0
  34. package/templates/next-intl/components/sections/Hero.tsx +24 -0
  35. package/templates/next-intl/i18n/navigation.ts +6 -0
  36. package/templates/next-intl/i18n/request.ts +17 -0
  37. package/templates/next-intl/i18n/routing.ts +17 -0
  38. package/templates/next-intl/next.config.ts +8 -0
  39. package/templates/next-intl/package.json +25 -0
  40. package/templates/next-intl/proxy.ts +11 -0
  41. package/templates/{next/lib → next-intl}/smoodly/admin.tsx +2 -2
  42. package/templates/{next → next-intl/smoodly}/collections.ts +1 -0
  43. package/templates/next-intl/smoodly/config.ts +31 -0
  44. package/templates/{next/lib → next-intl}/smoodly/ops.ts +3 -3
  45. package/templates/next-intl/smoodly/sections.ts +8 -0
  46. package/templates/next-intl/smoodly/server.ts +73 -0
  47. package/templates/next-intl/supabase/migrations/00000000000000_smoodly.sql +427 -0
  48. package/templates/next-intl/tsconfig.json +41 -0
  49. package/templates/next/app/[slug]/page.tsx +0 -42
  50. package/templates/next/lib/smoodly/index.ts +0 -64
  51. package/templates/next/sections.ts +0 -8
  52. package/templates/next/smoodly.config.ts +0 -19
@@ -0,0 +1,11 @@
1
+ import createMiddleware from "next-intl/middleware";
2
+ import { routing } from "./i18n/routing";
3
+
4
+ // next-intl's locale routing. /admin is excluded: the admin sits outside
5
+ // app/[locale] and has no locale prefix.
6
+ export default createMiddleware(routing);
7
+
8
+ export const config = {
9
+ // Everything except /admin, /api, Next internals and files with an extension.
10
+ matcher: "/((?!admin|api|_next|_vercel|.*\\..*).*)",
11
+ };
@@ -1,6 +1,6 @@
1
1
  import { createSmoodlyAdmin } from "smoodly/admin/next";
2
- import { config } from "../../smoodly.config";
3
- import { publicAuth } from "../smoodly";
2
+ import { config } from "./config";
4
3
  import { smoodlyOp } from "./ops";
4
+ import { publicAuth } from "./server";
5
5
 
6
6
  export const { AdminPage, AdminLayout } = createSmoodlyAdmin({ config, op: smoodlyOp, auth: publicAuth });
@@ -14,6 +14,7 @@ export const articles = smoodly.collection({
14
14
  name: "articles",
15
15
  title: "Articles",
16
16
  titleField: "title",
17
+ path: "posts",
17
18
  fields: {
18
19
  title: f.text(),
19
20
  slug: f.slug({ from: "title" }),
@@ -0,0 +1,31 @@
1
+ import { defineConfig, f } from "smoodly";
2
+ import { routing } from "@/i18n/routing";
3
+ import { collections } from "./collections";
4
+ import { sections } from "./sections";
5
+ import { HomePage } from "@/components/pages/Home";
6
+ import { StandardPage } from "@/components/pages/Page";
7
+ import { ArticlePage } from "@/components/pages/Article";
8
+
9
+ export { collections, sections };
10
+
11
+ // The registry and the plain settings, one file. Runtime-free: nothing
12
+ // here imports a route file or Next, so a script, the CLI's template
13
+ // build and (later) the MCP server can load it outside the app. The
14
+ // locale set comes from i18n/routing.ts (plain config, no Next inside) —
15
+ // the proxy runs in the edge runtime and could not import this file the
16
+ // other way round.
17
+ export const config = defineConfig({
18
+ registry: {
19
+ sections,
20
+ collections,
21
+ pages: [HomePage, StandardPage, ArticlePage],
22
+ },
23
+ locales: { default: routing.defaultLocale, supported: [...routing.locales] },
24
+ // Smoodly's own default, spelled out: the record the site root serves.
25
+ homePageSlug: "home",
26
+ // Nesting on: an editor can put pages three deep.
27
+ pages: { tree: { depth: 3 } },
28
+ styles: {
29
+ sections: { spacing: f.select(["sm", "md", "lg"]).default("md") },
30
+ },
31
+ });
@@ -3,9 +3,9 @@
3
3
  // gated by the editors table. ('use server' modules may only export
4
4
  // async functions — hence the shape.)
5
5
  import { createAdminOpHandler, type AdminOpCall } from "smoodly/admin/next";
6
- import { config } from "../../smoodly.config";
7
- import { adminAuth, stores } from "../smoodly";
6
+ import { config } from "./config";
7
+ import { adminAuth, href, stores } from "./server";
8
8
 
9
9
  export async function smoodlyOp(call: AdminOpCall) {
10
- return createAdminOpHandler({ config, ...stores(), auth: adminAuth() })(call);
10
+ return createAdminOpHandler({ config, ...stores(), auth: adminAuth(), href })(call);
11
11
  }
@@ -0,0 +1,8 @@
1
+ // The site's section registrations, in one list. config.ts puts it
2
+ // in the registry and re-exports it, so the config, the stores (which walk
3
+ // trees against these schemas) and the renderer all share one array.
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,73 @@
1
+ // Server-only glue: the client, both stores, the site layer and the
2
+ // renderer, constructed lazily so `next build` doesn't need env vars at
3
+ // import time. The env shape is the package's (SMOODLY_*); whether the
4
+ // secret is a service-role key or a cloud write key is invisible here.
5
+ import {
6
+ createSmoodlyClient, createSmoodlySite, smoodlyEnv, SupabaseEntryStore, SupabasePageStore, SupabasePathIndex,
7
+ } from "smoodly";
8
+ import { supabaseAdminAuth, type AdminAuth, type AuthConfig } from "smoodly/admin/next";
9
+ import { createPageRenderer, type PageRegistration } from "smoodly/next";
10
+ import type { SupabaseClient } from "@supabase/supabase-js";
11
+ import { getPathname } from "@/i18n/navigation";
12
+ import { collections, config, sections } from "./config";
13
+
14
+ let cached: { db: SupabaseClient; pages: SupabasePageStore; entries: SupabaseEntryStore; auth: AdminAuth } | null = null;
15
+
16
+ function bound() {
17
+ if (cached) return cached;
18
+ const env = smoodlyEnv();
19
+ const db = createSmoodlyClient(env);
20
+ // ONE path index for both stores: pages and entries share the URL space.
21
+ const paths = new SupabasePathIndex(db);
22
+ const locales = config.locales;
23
+ const entries = new SupabaseEntryStore(db, collections, { paths, locales });
24
+ const pages = new SupabasePageStore(db, {
25
+ sections, entries, paths, locales,
26
+ homePageSlug: config.homePageSlug,
27
+ tree: config.pages?.tree,
28
+ });
29
+ const auth = supabaseAdminAuth({ authUrl: env.authUrl, authKey: env.authKey, db });
30
+ cached = { db, pages, entries, auth };
31
+ return cached;
32
+ }
33
+
34
+ export function stores() {
35
+ const { pages, entries } = bound();
36
+ return { pages, entries };
37
+ }
38
+
39
+ /** The server-side client (seed script, one-off admin tasks). */
40
+ export const client = () => bound().db;
41
+
42
+ /** The admin gate: session verified against the auth project, editors row read with the write key. */
43
+ export const adminAuth = () => bound().auth;
44
+
45
+ /** What the browser needs to sign in — public values only. */
46
+ export function publicAuth(): AuthConfig {
47
+ const env = smoodlyEnv();
48
+ return { url: env.authUrl, key: env.authKey };
49
+ }
50
+
51
+ /** Locale-relative CMS path → site URL: the one seam for the renderer
52
+ * (metadata alternates, the `page` prop's links) and the admin (canvas,
53
+ * View live). The identity for a single-language site; with next-intl it
54
+ * becomes getPathname, which adds the locale prefix. */
55
+ export const href = (path: string, locale: string) => getPathname({ locale, href: path });
56
+
57
+ /** The site layer: (locale, path) → page or entry, and the content API
58
+ * (site.getPagesTree for menus, site.getEntries for lists). */
59
+ export const site = createSmoodlySite({
60
+ stores,
61
+ collections,
62
+ sections,
63
+ locales: config.locales,
64
+ homePageSlug: config.homePageSlug,
65
+ });
66
+
67
+ /** The two route files call these. */
68
+ export const { renderSmoodlyPath, smoodlyMetadata, renderSmoodlyPage, smoodlyPageMetadata } = createPageRenderer({
69
+ site,
70
+ sections,
71
+ pages: (config.registry.pages ?? []) as PageRegistration[],
72
+ href,
73
+ });
@@ -0,0 +1,427 @@
1
+ set client_min_messages = warning;
2
+
3
+ -- The space a request acts in: the key's claim in Smoodly Cloud, the
4
+ -- default space in self-host (service role, anon key). Policies, the
5
+ -- insert trigger and the reorder RPC all read this ONE function.
6
+ create or replace function "smoodly_current_space"() returns uuid
7
+ language sql stable as $$
8
+ select coalesce((auth.jwt() ->> 'space_id')::uuid, '00000000-0000-0000-0000-000000000000');
9
+ $$;
10
+
11
+ create table if not exists "pages" (
12
+ "id" uuid primary key default gen_random_uuid(),
13
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
14
+ "parent_id" uuid references "pages" ("id") on delete restrict,
15
+ "slug" text not null,
16
+ "title" text not null,
17
+ "i18n" jsonb,
18
+ "template" text,
19
+ "status" text not null default 'draft',
20
+ "sort" numeric,
21
+ "locales" text[],
22
+ "draft_version_id" uuid,
23
+ "published_version_id" uuid,
24
+ "created_at" timestamptz not null default now(),
25
+ "updated_at" timestamptz not null default now()
26
+ );
27
+
28
+ -- The tree is the URL (spec 2026-09-05 §1): a node stores its own segment
29
+ -- and its parent. Siblings are unique per segment; NULLS NOT DISTINCT so
30
+ -- two root nodes cannot share one either. template null = folder.
31
+ create unique index if not exists "pages_sibling_slug_key"
32
+ on "pages" ("space_id", "parent_id", "slug") nulls not distinct;
33
+
34
+ create index if not exists "pages_parent_idx"
35
+ on "pages" ("space_id", "parent_id", "sort");
36
+
37
+ -- Sibling reorder for pages: same shape as smoodly_reorder for entries.
38
+ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void language sql as $$
39
+ update "pages" pg set "sort" = u.ord
40
+ from unnest(p_ids) with ordinality as u(id, ord)
41
+ where pg."id" = u.id
42
+ and pg."space_id" = "smoodly_current_space"();
43
+ $$;
44
+
45
+ create table if not exists "page_versions" (
46
+ "id" uuid primary key default gen_random_uuid(),
47
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
48
+ "page_id" uuid not null references "pages" ("id") on delete cascade,
49
+ "tree" jsonb not null,
50
+ "created_by" text,
51
+ "created_at" timestamptz not null default now()
52
+ );
53
+
54
+ create table if not exists "saved_sections" (
55
+ "id" uuid primary key default gen_random_uuid(),
56
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
57
+ "title" text not null,
58
+ "fragment" jsonb not null,
59
+ "created_at" timestamptz not null default now()
60
+ );
61
+
62
+ create table if not exists "global_sections" (
63
+ "id" uuid primary key default gen_random_uuid(),
64
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
65
+ "type" text not null,
66
+ "content" jsonb not null,
67
+ "updated_at" timestamptz not null default now()
68
+ );
69
+
70
+ create table if not exists "entries" (
71
+ "id" uuid primary key default gen_random_uuid(),
72
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
73
+ "collection" text not null,
74
+ "parent_id" uuid references "entries" ("id") on delete restrict,
75
+ "fields" jsonb not null default '{}'::jsonb,
76
+ "i18n" jsonb,
77
+ "status" text not null default 'draft',
78
+ "sort" numeric,
79
+ "locales" text[],
80
+ "created_at" timestamptz not null default now(),
81
+ "updated_at" timestamptz not null default now()
82
+ );
83
+
84
+ -- Entry slugs are unique among SIBLINGS, like pages: one rule for every
85
+ -- node in the tree, and the URL is the tree. NULLS NOT DISTINCT so two
86
+ -- ROOT entries (parent_id null) cannot share a slug either. The partial
87
+ -- predicate keeps absent, null, empty or non-string slugs out of the
88
+ -- index entirely: with NULLS NOT DISTINCT a null slug would otherwise
89
+ -- collide with the next null slug, and a collection with no slug field
90
+ -- at all could hold only one entry per parent. jsonb_typeof(...) =
91
+ -- 'string' is required because ->> renders ANY JSON scalar as text —
92
+ -- a number, boolean or null slug would otherwise print as non-empty
93
+ -- text and claim the index; only a non-empty string slug claims a URL,
94
+ -- absent, null, empty or non-string slugs never conflict — the same
95
+ -- rule as slugOf in paths.ts. A flat collection (every entry a root)
96
+ -- therefore behaves exactly as per-collection uniqueness did.
97
+ create unique index if not exists "entries_sibling_slug_key"
98
+ on "entries" ("space_id", "collection", "parent_id", ("fields"->>'slug')) nulls not distinct
99
+ where (jsonb_typeof("fields"->'slug') = 'string' and "fields"->>'slug' <> '');
100
+
101
+ create index if not exists "entries_collection_status_idx"
102
+ on "entries" ("space_id", "collection", "status");
103
+
104
+ create index if not exists "entries_collection_sort_idx"
105
+ on "entries" ("space_id", "collection", "sort");
106
+
107
+ create index if not exists "entries_parent_idx"
108
+ on "entries" ("space_id", "collection", "parent_id");
109
+
110
+ -- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
111
+ -- statement. Deliberately does not touch updated_at — a drag must not
112
+ -- make the whole collection look freshly edited. Scoped to the current
113
+ -- space by the same function the policies use.
114
+ create or replace function "smoodly_reorder"(
115
+ p_collection text,
116
+ p_ids uuid[]
117
+ ) returns void language sql as $$
118
+ update "entries" e set "sort" = u.ord
119
+ from unnest(p_ids) with ordinality as u(id, ord)
120
+ where e."id" = u.id
121
+ and e."collection" = p_collection
122
+ and e."space_id" = "smoodly_current_space"();
123
+ $$;
124
+
125
+ create table if not exists "refs" (
126
+ "source_kind" text not null,
127
+ "source_id" uuid not null,
128
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
129
+ "source_field" text not null,
130
+ "target_collection" text not null,
131
+ "target_id" uuid not null,
132
+ primary key ("source_kind", "source_id", "source_field", "target_id")
133
+ );
134
+
135
+ -- The URL index (spec 2026-09-05 §4): the refs counterpart for paths.
136
+ -- Derived — the stores rewrite a target's rows on every create, rename,
137
+ -- move and delete — so one primary-key hit resolves any URL in any locale
138
+ -- and no page or entry can share a URL with another in any locale.
139
+ -- Rows exist for drafts too (the editor needs them); the read policy
140
+ -- admits a row only when its target is published.
141
+ create table if not exists "paths" (
142
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
143
+ "locale" text not null,
144
+ "path" text not null,
145
+ "kind" text not null check ("kind" in ('page', 'entry')),
146
+ "target_id" uuid not null,
147
+ primary key ("space_id", "locale", "path")
148
+ );
149
+
150
+ create index if not exists "paths_target_idx"
151
+ on "paths" ("space_id", "kind", "target_id");
152
+
153
+ -- Replace the targets' rows with the given rows in ONE call: a SQL
154
+ -- function runs in the caller's transaction, so a primary-key collision
155
+ -- rolls the whole rewrite back. Inserted rows get their space from the
156
+ -- a_set_space trigger (the claim, or the default space).
157
+ create or replace function "smoodly_rewrite_paths"(p_targets jsonb, p_rows jsonb) returns void language sql as $$
158
+ delete from "paths" p
159
+ using jsonb_to_recordset(p_targets) as t(kind text, id uuid)
160
+ where p."kind" = t.kind and p."target_id" = t.id
161
+ and p."space_id" = "smoodly_current_space"();
162
+ insert into "paths" ("locale", "path", "kind", "target_id")
163
+ select r.locale, r.path, r.kind, r.id
164
+ from jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid);
165
+ $$;
166
+
167
+ -- RLS on every core table. The service role bypasses it (self-host
168
+ -- adapters untouched). The policy set that follows (space layer) admits
169
+ -- a key to its own space: everything for a write key, published rows for
170
+ -- a read key or the anon key (default space). Drafts stay unreachable
171
+ -- to anything but a write key. The generated per-collection views
172
+ -- (collectionSQL) run with security_invoker = true, so they are subject
173
+ -- to this same smoodly_read policy rather than their owner's privileges.
174
+ alter table "pages" enable row level security;
175
+ alter table "page_versions" enable row level security;
176
+ alter table "saved_sections" enable row level security;
177
+ alter table "global_sections" enable row level security;
178
+ alter table "entries" enable row level security;
179
+ alter table "refs" enable row level security;
180
+ alter table "paths" enable row level security;
181
+
182
+ -- ── Cloud state (spec 2026-09-04 §1): three ordinary tables, shipped
183
+ -- for everyone with ONE definition. Self-host fills them by hand or
184
+ -- leaves them empty; smoodly.io writes rows with the service role.
185
+ -- There is never cloud-only SQL on a content database.
186
+ create table if not exists "editors" (
187
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
188
+ "user_id" uuid not null,
189
+ "role" text not null default 'editor',
190
+ primary key ("space_id", "user_id")
191
+ );
192
+
193
+ create table if not exists "space_keys" (
194
+ "id" uuid primary key,
195
+ "space_id" uuid not null,
196
+ "kind" text not null check ("kind" in ('read', 'write')),
197
+ "revoked_at" timestamptz
198
+ );
199
+
200
+ create table if not exists "space_caps" (
201
+ "space_id" uuid primary key,
202
+ "row_cap" integer not null
203
+ );
204
+
205
+ alter table "editors" enable row level security;
206
+ alter table "space_keys" enable row level security;
207
+ alter table "space_caps" enable row level security;
208
+
209
+ -- ── Claim readers. The two lookups run as definer so they can read the
210
+ -- service-role-only tables from inside a policy; search_path is pinned.
211
+ create or replace function "smoodly_key_kind"() returns text
212
+ language sql stable as $$
213
+ select auth.jwt() ->> 'kind';
214
+ $$;
215
+
216
+ create or replace function "smoodly_space_key_revoked"() returns boolean
217
+ language sql stable security definer set search_path = public as $$
218
+ select coalesce(
219
+ (select k."revoked_at" is not null from "space_keys" k
220
+ where k."id" = (auth.jwt() ->> 'key_id')::uuid),
221
+ false);
222
+ $$;
223
+
224
+ create or replace function "smoodly_space_row_cap"() returns integer
225
+ language sql stable security definer set search_path = public as $$
226
+ select c."row_cap" from "space_caps" c where c."space_id" = "smoodly_current_space"();
227
+ $$;
228
+
229
+ -- ── Triggers. A key's claim wins over whatever the insert said, so an
230
+ -- adapter never has to know its space and cannot write into another.
231
+ -- No claim (service role, anon) leaves the given or default value —
232
+ -- that is how smoodly.io writes cloud-state rows for a chosen space.
233
+ create or replace function "smoodly_set_space"() returns trigger
234
+ language plpgsql as $$
235
+ begin
236
+ if auth.jwt() ->> 'space_id' is not null then
237
+ new."space_id" := (auth.jwt() ->> 'space_id')::uuid;
238
+ end if;
239
+ return new;
240
+ end;
241
+ $$;
242
+
243
+ -- Per table, per space. No space_caps row = no cap (self-host). Trigger
244
+ -- a_ (smoodly_set_space) runs first, so new.space_id already equals the
245
+ -- current space for a keyed insert; smoodly_space_row_cap() reads the
246
+ -- current space itself, so this never needs the row's space_id as an
247
+ -- argument (which would let one space's insert probe another's cap).
248
+ -- A service-role insert that names another space explicitly is a
249
+ -- different case: there is no JWT claim, so smoodly_current_space()
250
+ -- still resolves to the default space, and the cap read is the default
251
+ -- space's (none) — the service role is never capped by another space's row.
252
+ -- search_path is pinned empty, so every reference below is schema-qualified.
253
+ create or replace function "smoodly_enforce_row_cap"() returns trigger
254
+ language plpgsql set search_path = '' as $$
255
+ declare
256
+ cap integer;
257
+ n bigint;
258
+ begin
259
+ cap := public."smoodly_space_row_cap"();
260
+ if cap is null then
261
+ return new;
262
+ end if;
263
+ -- Advisory only (free-tier abuse guard), not a hard invariant: two
264
+ -- concurrent inserts can both observe n = cap - 1 and both pass.
265
+ execute format('select count(*) from %I.%I where "space_id" = $1', tg_table_schema, tg_table_name)
266
+ into n using new."space_id";
267
+ if n >= cap then
268
+ raise exception 'smoodly: row cap (%) reached for this space in %', cap, tg_table_name
269
+ using errcode = 'check_violation';
270
+ end if;
271
+ return new;
272
+ end;
273
+ $$;
274
+
275
+ create index if not exists "page_versions_space_idx" on "page_versions" ("space_id");
276
+
277
+ create index if not exists "saved_sections_space_idx" on "saved_sections" ("space_id");
278
+
279
+ create index if not exists "global_sections_space_idx" on "global_sections" ("space_id");
280
+
281
+ create index if not exists "refs_space_idx" on "refs" ("space_id");
282
+
283
+ drop trigger if exists "smoodly_a_set_space" on "pages";
284
+ create trigger "smoodly_a_set_space" before insert on "pages"
285
+ for each row execute function "smoodly_set_space"();
286
+
287
+ drop trigger if exists "smoodly_a_set_space" on "page_versions";
288
+ create trigger "smoodly_a_set_space" before insert on "page_versions"
289
+ for each row execute function "smoodly_set_space"();
290
+
291
+ drop trigger if exists "smoodly_a_set_space" on "saved_sections";
292
+ create trigger "smoodly_a_set_space" before insert on "saved_sections"
293
+ for each row execute function "smoodly_set_space"();
294
+
295
+ drop trigger if exists "smoodly_a_set_space" on "global_sections";
296
+ create trigger "smoodly_a_set_space" before insert on "global_sections"
297
+ for each row execute function "smoodly_set_space"();
298
+
299
+ drop trigger if exists "smoodly_a_set_space" on "entries";
300
+ create trigger "smoodly_a_set_space" before insert on "entries"
301
+ for each row execute function "smoodly_set_space"();
302
+
303
+ drop trigger if exists "smoodly_a_set_space" on "refs";
304
+ create trigger "smoodly_a_set_space" before insert on "refs"
305
+ for each row execute function "smoodly_set_space"();
306
+
307
+ drop trigger if exists "smoodly_a_set_space" on "paths";
308
+ create trigger "smoodly_a_set_space" before insert on "paths"
309
+ for each row execute function "smoodly_set_space"();
310
+
311
+ drop trigger if exists "smoodly_a_set_space" on "editors";
312
+ create trigger "smoodly_a_set_space" before insert on "editors"
313
+ for each row execute function "smoodly_set_space"();
314
+
315
+ drop trigger if exists "smoodly_b_row_cap" on "pages";
316
+ create trigger "smoodly_b_row_cap" before insert on "pages"
317
+ for each row execute function "smoodly_enforce_row_cap"();
318
+
319
+ drop trigger if exists "smoodly_b_row_cap" on "page_versions";
320
+ create trigger "smoodly_b_row_cap" before insert on "page_versions"
321
+ for each row execute function "smoodly_enforce_row_cap"();
322
+
323
+ drop trigger if exists "smoodly_b_row_cap" on "saved_sections";
324
+ create trigger "smoodly_b_row_cap" before insert on "saved_sections"
325
+ for each row execute function "smoodly_enforce_row_cap"();
326
+
327
+ drop trigger if exists "smoodly_b_row_cap" on "global_sections";
328
+ create trigger "smoodly_b_row_cap" before insert on "global_sections"
329
+ for each row execute function "smoodly_enforce_row_cap"();
330
+
331
+ drop trigger if exists "smoodly_b_row_cap" on "entries";
332
+ create trigger "smoodly_b_row_cap" before insert on "entries"
333
+ for each row execute function "smoodly_enforce_row_cap"();
334
+
335
+ drop trigger if exists "smoodly_b_row_cap" on "refs";
336
+ create trigger "smoodly_b_row_cap" before insert on "refs"
337
+ for each row execute function "smoodly_enforce_row_cap"();
338
+
339
+ drop trigger if exists "smoodly_b_row_cap" on "paths";
340
+ create trigger "smoodly_b_row_cap" before insert on "paths"
341
+ for each row execute function "smoodly_enforce_row_cap"();
342
+
343
+ drop policy if exists "smoodly_write" on "pages";
344
+ create policy "smoodly_write" on "pages" for all
345
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
346
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
347
+
348
+ drop policy if exists "smoodly_read" on "pages";
349
+ create policy "smoodly_read" on "pages" for select
350
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
351
+
352
+ drop policy if exists "smoodly_write" on "page_versions";
353
+ create policy "smoodly_write" on "page_versions" for all
354
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
355
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
356
+
357
+ drop policy if exists "smoodly_read" on "page_versions";
358
+ create policy "smoodly_read" on "page_versions" for select
359
+ 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"));
360
+
361
+ drop policy if exists "smoodly_write" on "saved_sections";
362
+ create policy "smoodly_write" on "saved_sections" for all
363
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
364
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
365
+
366
+ drop policy if exists "smoodly_write" on "global_sections";
367
+ create policy "smoodly_write" on "global_sections" for all
368
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
369
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
370
+
371
+ drop policy if exists "smoodly_read" on "global_sections";
372
+ create policy "smoodly_read" on "global_sections" for select
373
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()));
374
+
375
+ drop policy if exists "smoodly_write" on "entries";
376
+ create policy "smoodly_write" on "entries" for all
377
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
378
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
379
+
380
+ drop policy if exists "smoodly_read" on "entries";
381
+ create policy "smoodly_read" on "entries" for select
382
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
383
+
384
+ drop policy if exists "smoodly_write" on "refs";
385
+ create policy "smoodly_write" on "refs" for all
386
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
387
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
388
+
389
+ drop policy if exists "smoodly_write" on "paths";
390
+ create policy "smoodly_write" on "paths" for all
391
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
392
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
393
+
394
+ drop policy if exists "smoodly_read" on "paths";
395
+ create policy "smoodly_read" on "paths" for select
396
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (("kind" = 'page' and exists (select 1 from "pages" p where p."id" = "paths"."target_id" and p."status" = 'published')) or ("kind" = 'entry' and exists (select 1 from "entries" e where e."id" = "paths"."target_id" and e."status" = 'published'))));
397
+
398
+ drop policy if exists "smoodly_write" on "editors";
399
+ create policy "smoodly_write" on "editors" for all
400
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
401
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
402
+
403
+ create or replace view "people" with (security_invoker = true) as
404
+ select
405
+ "id",
406
+ "sort",
407
+ "locales",
408
+ "created_at",
409
+ "updated_at",
410
+ "fields"->>'name' as "name",
411
+ "fields"->>'role' as "role"
412
+ from "entries"
413
+ where "collection" = 'people' and "status" = 'published';
414
+
415
+ create or replace view "articles" with (security_invoker = true) as
416
+ select
417
+ "id",
418
+ "sort",
419
+ "locales",
420
+ "created_at",
421
+ "updated_at",
422
+ "fields"->>'title' as "title",
423
+ "fields"->>'slug' as "slug",
424
+ "fields"->>'excerpt' as "excerpt",
425
+ "fields"->>'author' as "author"
426
+ from "entries"
427
+ where "collection" = 'articles' and "status" = 'published';
@@ -0,0 +1,41 @@
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
+ "paths": {
21
+ "@/*": [
22
+ "./*"
23
+ ]
24
+ },
25
+ "plugins": [
26
+ {
27
+ "name": "next"
28
+ }
29
+ ]
30
+ },
31
+ "include": [
32
+ "next-env.d.ts",
33
+ "**/*.ts",
34
+ "**/*.tsx",
35
+ ".next/types/**/*.ts",
36
+ ".next/dev/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules"
40
+ ]
41
+ }