@plutocms/supabase 0.0.1-alpha.1 → 0.0.1-alpha.11

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/public/schema.sql CHANGED
@@ -14,7 +14,7 @@ end
14
14
  $$;
15
15
 
16
16
  --- Create settings table
17
- create table public.settings (
17
+ create table if not exists public.settings (
18
18
  id bigint generated by default as identity not null,
19
19
  setting_name public.TSettings not null,
20
20
  setting_value text not null,
@@ -26,21 +26,60 @@ create table public.settings (
26
26
  insert into public.settings (setting_name, setting_value) values
27
27
  ('website_title', 'My Website'),
28
28
  ('website_url', 'https://mywebsite.test'),
29
- ('website_description', 'This is my website.');
29
+ ('website_description', 'This is my website.')
30
+ on conflict (setting_name) do nothing;
30
31
 
31
32
  alter table public.settings enable row level security;
32
33
 
33
34
  -- Policies for settings table
34
- create policy "Enable insert for authenticated users only"
35
- on public.settings
36
- to authenticated, dashboard_user
37
- with check (true);
35
+ do $$
36
+ begin
37
+ if not exists (
38
+ select 1 from pg_policies where tablename = 'settings' and policyname = 'Enable insert for authenticated users only'
39
+ ) then
40
+ create policy "Enable insert for authenticated users only"
41
+ on public.settings
42
+ for insert
43
+ to authenticated, dashboard_user
44
+ with check (true);
45
+ end if;
46
+ end
47
+ $$;
48
+
49
+ do $$
50
+ begin
51
+ if not exists (
52
+ select 1 from pg_policies where tablename = 'settings' and policyname = 'Enable read access for authenticated users on settings'
53
+ ) then
54
+ create policy "Enable read access for authenticated users on settings"
55
+ on public.settings
56
+ for select
57
+ to authenticated, dashboard_user
58
+ using (true);
59
+ end if;
60
+ end
61
+ $$;
62
+
63
+ do $$
64
+ begin
65
+ if not exists (
66
+ select 1 from pg_policies where tablename = 'settings' and policyname = 'Enable update for authenticated users on settings'
67
+ ) then
68
+ create policy "Enable update for authenticated users on settings"
69
+ on public.settings
70
+ for update
71
+ to authenticated, dashboard_user
72
+ using (true)
73
+ with check (true);
74
+ end if;
75
+ end
76
+ $$;
38
77
 
39
78
  --- Create enum type for healthcheck names
40
79
  do $$
41
80
  begin
42
81
  if not exists (select 1 from pg_type where typname = 'thealthcheck') then
43
- create type Healthcheck as enum (
82
+ create type THealthcheck as enum (
44
83
  'first_setup'
45
84
  );
46
85
  end if;
@@ -48,56 +87,140 @@ end
48
87
  $$;
49
88
 
50
89
  --- Create healthcheck table (name/value format like settings)
51
- create table public.healthcheck (
90
+ create table if not exists public.healthcheck (
52
91
  id bigint generated by default as identity not null,
53
- check_name public.Healthcheck not null,
54
- check_value text not null,
92
+ config_name public.THealthcheck not null,
93
+ config_value text not null,
55
94
  constraint healthcheck_pkey primary key (id),
56
- constraint healthcheck_check_name_key unique (check_name)
95
+ constraint healthcheck_config_name_key unique (config_name)
57
96
  ) TABLESPACE pg_default;
58
97
 
59
98
  --- Insert default healthcheck values
60
- insert into public.healthcheck (check_name, check_value) values
61
- ('first_setup', 'true');
99
+ insert into public.healthcheck (config_name, config_value) values
100
+ ('first_setup', 'true')
101
+ on conflict (config_name) do nothing;
62
102
 
63
103
  alter table public.healthcheck enable row level security;
64
104
 
65
105
  -- Allow read access to healthcheck for public and dashboard_user (no login required)
66
- create policy "Enable read access for healthcheck to public and dashboard_user"
67
- on public.healthcheck
68
- for select
69
- to public, dashboard_user
70
- using (true);
106
+ do $$
107
+ begin
108
+ if not exists (
109
+ select 1 from pg_policies where tablename = 'healthcheck' and policyname = 'Enable read access for healthcheck to public'
110
+ ) then
111
+ create policy "Enable read access for healthcheck to public"
112
+ on public.healthcheck
113
+ for select
114
+ to public
115
+ using (true);
116
+ end if;
117
+ end
118
+ $$;
119
+
120
+ -- Allow update access to healthcheck for authenticated users and dashboard_user
121
+ do $$
122
+ begin
123
+ if not exists (
124
+ select 1 from pg_policies where tablename = 'healthcheck' and policyname = 'Enable update for authenticated users on healthcheck'
125
+ ) then
126
+ create policy "Enable update for authenticated users on healthcheck"
127
+ on public.healthcheck
128
+ for update
129
+ to authenticated, dashboard_user
130
+ using (true)
131
+ with check (true);
132
+ end if;
133
+ end
134
+ $$;
71
135
 
72
136
  -- Profiles
73
- create table profiles (
137
+ create table if not exists public.profiles (
74
138
  id uuid references auth.users on delete cascade not null primary key,
75
139
  updated_at timestamp with time zone,
76
- first_name text,
77
- last_name text,
78
- constraint first_name_length check (char_length(first_name) >= 3)
140
+ email text,
141
+ username text,
142
+ display_name text,
143
+ is_admin boolean not null default false,
144
+ constraint username_length check (char_length(username) >= 3)
79
145
  );
80
146
 
81
147
  alter table public.profiles enable row level security;
82
148
 
83
- create policy "Enable read access for authenticated users"
84
- on public.profiles
85
- to authenticated, dashboard_user
86
- for select using (true);
149
+ do $$
150
+ begin
151
+ if not exists (
152
+ select 1 from pg_policies where tablename = 'profiles' and policyname = 'Enable read access for authenticated users'
153
+ ) then
154
+ create policy "Enable read access for authenticated users"
155
+ on public.profiles
156
+ for select
157
+ to authenticated, dashboard_user
158
+ using (true);
159
+ end if;
160
+ end
161
+ $$;
87
162
 
88
163
  --- inserts a row into public.profiles
89
- create function public.handle_new_user()
164
+ --- automatically grants admin to the first user if no profiles exist yet
165
+ create or replace function public.handle_new_user()
90
166
  returns trigger
91
167
  language plpgsql
92
168
  security definer set search_path = ''
93
169
  as $$
170
+ declare
171
+ profile_count int;
172
+ should_be_admin boolean;
94
173
  begin
95
- insert into public.profiles (id, first_name, last_name)
96
- values (new.id, new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data ->> 'last_name');
174
+ select count(*) into profile_count from public.profiles;
175
+
176
+ should_be_admin := profile_count = 0
177
+ or coalesce((new.raw_user_meta_data ->> 'is_admin')::boolean, false);
178
+
179
+ insert into public.profiles (id, email, username, display_name, is_admin)
180
+ values (
181
+ new.id,
182
+ new.email,
183
+ new.raw_user_meta_data ->> 'username',
184
+ new.raw_user_meta_data ->> 'display_name',
185
+ should_be_admin
186
+ );
97
187
  return new;
98
188
  end;
99
189
  $$;
100
190
  --- trigger the function every time a user is created
101
- create trigger on_auth_user_created
102
- after insert on auth.users
103
- for each row execute procedure public.handle_new_user();
191
+ do $$
192
+ begin
193
+ if not exists (
194
+ select 1 from pg_trigger where tgname = 'on_auth_user_created'
195
+ ) then
196
+ create trigger on_auth_user_created
197
+ after insert on auth.users
198
+ for each row execute procedure public.handle_new_user();
199
+ end if;
200
+ end
201
+ $$;
202
+
203
+ -- Migrations tracking
204
+ create table if not exists public.pluto_migrations (
205
+ id bigint generated by default as identity primary key,
206
+ layer_name text not null,
207
+ applied_at timestamptz not null default now(),
208
+ constraint pluto_migrations_layer_key unique (layer_name)
209
+ );
210
+
211
+ alter table public.pluto_migrations enable row level security;
212
+
213
+ -- Only authenticated users can read migrations
214
+ do $$
215
+ begin
216
+ if not exists (
217
+ select 1 from pg_policies where tablename = 'pluto_migrations' and policyname = 'Enable read access for authenticated users on pluto_migrations'
218
+ ) then
219
+ create policy "Enable read access for authenticated users on pluto_migrations"
220
+ on public.pluto_migrations
221
+ for select
222
+ to authenticated, dashboard_user
223
+ using (true);
224
+ end if;
225
+ end
226
+ $$;
@@ -10,7 +10,7 @@ export default defineEventHandler(async (event) => {
10
10
 
11
11
  if (error) {
12
12
  return {
13
- success: false,
13
+ success: false as const,
14
14
  message: 'Error retrieving healthcheck settings',
15
15
  error: error.message,
16
16
  }
@@ -32,7 +32,7 @@ export default defineEventHandler(async (event) => {
32
32
  })
33
33
 
34
34
  return {
35
- success: true,
35
+ success: true as const,
36
36
  message: 'Healthcheck settings retrieved successfully',
37
37
  is_first_setup: isFirstSetup.first_setup === 'true',
38
38
  }
@@ -1,4 +1,7 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
1
3
  import postgres from 'postgres'
4
+ import { splitStatements } from '../../utils/sql'
2
5
 
3
6
  interface Payload {
4
7
  baseUrl: string
@@ -30,16 +33,47 @@ export default defineEventHandler(async (event) => {
30
33
  const sql = postgres(connectionString)
31
34
 
32
35
  try {
33
- /* await sql.begin(async (sql) => {
34
- await sql.unsafe(schema)
35
- }) */
36
-
37
- // Set the first_setup flag in the healthcheck table to false
38
- await sql`
39
- UPDATE healthcheck
40
- SET config_value = 'false'
41
- WHERE config_name = 'first_setup';
42
- `
36
+ const statements = splitStatements(schema)
37
+
38
+ for (const statement of statements) {
39
+ await sql.unsafe(statement)
40
+ }
41
+
42
+ // Mark first_setup as complete
43
+ await sql.unsafe(
44
+ `UPDATE public.healthcheck SET config_value = 'false' WHERE config_name = 'first_setup'`
45
+ )
46
+
47
+ // Record the core schema migration
48
+ await sql.unsafe(
49
+ `INSERT INTO public.pluto_migrations (layer_name)
50
+ VALUES ('core')
51
+ ON CONFLICT (layer_name) DO NOTHING`
52
+ )
53
+
54
+ // Persist connection string to .env for future layer migrations
55
+ const envPath = resolve(process.cwd(), '.env')
56
+ let envContent = ''
57
+ try {
58
+ envContent = await readFile(envPath, 'utf-8')
59
+ } catch {
60
+ // .env doesn't exist yet
61
+ }
62
+
63
+ if (envContent.includes('DATABASE_URL=')) {
64
+ envContent = envContent.replace(
65
+ /^DATABASE_URL=.*$/m,
66
+ `DATABASE_URL="${connectionString}"`
67
+ )
68
+ } else {
69
+ // Ensure there's a trailing newline before appending
70
+ if (envContent.length > 0 && !envContent.endsWith('\n')) {
71
+ envContent += '\n'
72
+ }
73
+ envContent += `DATABASE_URL="${connectionString}"\n`
74
+ }
75
+
76
+ await writeFile(envPath, envContent, 'utf-8')
43
77
 
44
78
  return {
45
79
  success: true,
@@ -1,28 +1,46 @@
1
1
  import { serverSupabaseClient } from '#supabase/server'
2
2
 
3
+ interface Payload {
4
+ username: string
5
+ display_name: string
6
+ email: string
7
+ password: string
8
+ }
9
+
3
10
  export default defineEventHandler(async (event) => {
4
11
  const client = await serverSupabaseClient<Database>(event)
5
12
 
6
- const body = await readBody(event)
13
+ const body = await readBody<Payload>(event)
14
+
15
+ if (!body.username || body.username.length < 3) {
16
+ return {
17
+ success: false,
18
+ message: 'Username must be at least 3 characters.',
19
+ }
20
+ }
7
21
 
8
22
  const { data, error } = await client.auth.signUp({
9
23
  email: body.email,
10
24
  password: body.password,
11
25
  options: {
12
26
  data: {
13
- first_name: body.first_name,
14
- last_name: body.last_name,
27
+ username: body.username,
28
+ display_name: body.display_name,
15
29
  },
16
30
  },
17
31
  })
18
32
 
19
33
  if (error) {
20
- throw createError({
21
- statusCode: Number.parseInt(error.code ?? '500'),
22
- cause: error.cause,
34
+ return {
35
+ success: false,
23
36
  message: error.message,
24
- })
37
+ error,
38
+ }
25
39
  }
26
40
 
27
- return { message: 'User registered successfully', data }
41
+ return {
42
+ success: true,
43
+ message: 'User registered successfully',
44
+ data,
45
+ }
28
46
  })
@@ -0,0 +1,48 @@
1
+ import { runLayerMigration } from '../utils/migrations'
2
+
3
+ export default defineNitroPlugin(async () => {
4
+ // Only run if DATABASE_URL is configured (setup already completed)
5
+ if (!process.env.DATABASE_URL) {
6
+ return
7
+ }
8
+
9
+ const config = useRuntimeConfig()
10
+ const layerSchemas: Record<string, string> =
11
+ (config as any).plutoLayerSchemas ?? {}
12
+
13
+ const layerNames = Object.keys(layerSchemas)
14
+
15
+ if (layerNames.length === 0) {
16
+ return
17
+ }
18
+
19
+ for (const layerName of layerNames) {
20
+ const schemaSql = layerSchemas[layerName]
21
+
22
+ if (!schemaSql) {
23
+ continue
24
+ }
25
+
26
+ try {
27
+ const result = await runLayerMigration({
28
+ layerName,
29
+ schemaSql,
30
+ })
31
+
32
+ if (result.skipped) {
33
+ console.warn(
34
+ `[migrations] Layer "${layerName}" already applied, skipped.`
35
+ )
36
+ } else if (result.success) {
37
+ console.warn(`[migrations] Layer "${layerName}" migrated successfully.`)
38
+ } else {
39
+ console.error(`[migrations] Layer "${layerName}" failed:`, result.error)
40
+ }
41
+ } catch (error) {
42
+ console.error(
43
+ `[migrations] Error running migration for "${layerName}":`,
44
+ error
45
+ )
46
+ }
47
+ }
48
+ })
@@ -0,0 +1,88 @@
1
+ import postgres from 'postgres'
2
+ import { splitStatements } from './sql'
3
+
4
+ /**
5
+ * Retrieves the database connection string from environment variables.
6
+ * Stored in .env during initial setup.
7
+ */
8
+ export function getConnectionString(): string | null {
9
+ return process.env.DATABASE_URL ?? null
10
+ }
11
+
12
+ /**
13
+ * Checks if a specific layer migration has already been applied.
14
+ */
15
+ async function isMigrationApplied(
16
+ sql: postgres.Sql,
17
+ layerName: string
18
+ ): Promise<boolean> {
19
+ const result = await sql.unsafe(
20
+ `SELECT 1 FROM public.pluto_migrations WHERE layer_name = $1`,
21
+ [layerName]
22
+ )
23
+ return result.length > 0
24
+ }
25
+
26
+ /**
27
+ * Records a migration as applied.
28
+ */
29
+ async function recordMigration(
30
+ sql: postgres.Sql,
31
+ layerName: string
32
+ ): Promise<void> {
33
+ await sql.unsafe(
34
+ `INSERT INTO public.pluto_migrations (layer_name)
35
+ VALUES ($1)
36
+ ON CONFLICT (layer_name) DO NOTHING`,
37
+ [layerName]
38
+ )
39
+ }
40
+
41
+ interface LayerMigrationResult {
42
+ success: boolean
43
+ skipped?: boolean
44
+ error?: string
45
+ }
46
+
47
+ /**
48
+ * Runs a layer's schema SQL if not already applied.
49
+ * Splits and executes statements, then records the migration.
50
+ */
51
+ export async function runLayerMigration(opts: {
52
+ layerName: string
53
+ schemaSql: string
54
+ connectionString?: string
55
+ }): Promise<LayerMigrationResult> {
56
+ const connStr = opts.connectionString ?? getConnectionString()
57
+
58
+ if (!connStr) {
59
+ return {
60
+ success: false,
61
+ error: 'No DATABASE_URL configured. Re-run setup or add it to .env.',
62
+ }
63
+ }
64
+
65
+ const sql = postgres(connStr)
66
+
67
+ try {
68
+ const applied = await isMigrationApplied(sql, opts.layerName)
69
+ if (applied) {
70
+ return { success: true, skipped: true }
71
+ }
72
+
73
+ const statements = splitStatements(opts.schemaSql)
74
+
75
+ for (const statement of statements) {
76
+ await sql.unsafe(statement)
77
+ }
78
+
79
+ await recordMigration(sql, opts.layerName)
80
+
81
+ return { success: true }
82
+ } catch (error: any) {
83
+ console.error(`Migration error [${opts.layerName}]:`, error)
84
+ return { success: false, error: error.message }
85
+ } finally {
86
+ await sql.end()
87
+ }
88
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Splits a SQL string into individual statements, correctly handling
3
+ * $$ dollar-quoted blocks, single-quoted strings, and -- comments.
4
+ */
5
+ export function splitStatements(sql: string): string[] {
6
+ const statements: string[] = []
7
+ let current = ''
8
+ let i = 0
9
+
10
+ while (i < sql.length) {
11
+ // Check for dollar-quoting ($$)
12
+ if (sql[i] === '$' && sql[i + 1] === '$') {
13
+ current += '$$'
14
+ i += 2
15
+ // Read until closing $$
16
+ while (i < sql.length) {
17
+ if (sql[i] === '$' && sql[i + 1] === '$') {
18
+ current += '$$'
19
+ i += 2
20
+ break
21
+ }
22
+ current += sql[i]
23
+ i++
24
+ }
25
+ continue
26
+ }
27
+
28
+ // Check for single-quoted strings (handle '' escapes)
29
+ if (sql[i] === `'`) {
30
+ current += sql[i]
31
+ i++
32
+ while (i < sql.length) {
33
+ if (sql[i] === `'` && sql[i + 1] === `'`) {
34
+ // Escaped quote
35
+ current += `''`
36
+ i += 2
37
+ continue
38
+ }
39
+ if (sql[i] === `'`) {
40
+ current += sql[i]
41
+ i++
42
+ break
43
+ }
44
+ current += sql[i]
45
+ i++
46
+ }
47
+ continue
48
+ }
49
+
50
+ // Check for single-line comments
51
+ if (sql[i] === '-' && sql[i + 1] === '-') {
52
+ while (i < sql.length && sql[i] !== '\n') {
53
+ current += sql[i]
54
+ i++
55
+ }
56
+ continue
57
+ }
58
+
59
+ // Statement terminator
60
+ if (sql[i] === ';') {
61
+ current += ';'
62
+ const trimmed = current.trim()
63
+ if (trimmed && trimmed !== ';') {
64
+ statements.push(trimmed)
65
+ }
66
+ current = ''
67
+ i++
68
+ continue
69
+ }
70
+
71
+ current += sql[i]
72
+ i++
73
+ }
74
+
75
+ const trimmed = current.trim()
76
+ if (trimmed && trimmed !== ';') {
77
+ statements.push(trimmed)
78
+ }
79
+
80
+ return statements
81
+ }