@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/app/components/navbar/NavbarAdmin.vue +5 -5
- package/app/composables/auth.ts +17 -1
- package/app/middleware/auth.ts +5 -1
- package/app/middleware/setup-check.ts +30 -18
- package/app/pages/admin/(auth)/signup.vue +16 -10
- package/app/pages/admin/setup.vue +30 -27
- package/app/pages/admin.vue +1 -0
- package/bun.lock +160 -8
- package/modules/pluto-migrations.ts +71 -0
- package/nuxt.config.ts +13 -19
- package/package.json +6 -4
- package/public/schema.sql +155 -32
- package/server/api/settings/first_setup.get.ts +2 -2
- package/server/api/setup/create.post.ts +44 -10
- package/server/api/signup.post.ts +26 -8
- package/server/plugins/migrations.ts +48 -0
- package/server/utils/migrations.ts +88 -0
- package/server/utils/sql.ts +81 -0
- package/shared/types/supabase.ts +78 -117
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
92
|
+
config_name public.THealthcheck not null,
|
|
93
|
+
config_value text not null,
|
|
55
94
|
constraint healthcheck_pkey primary key (id),
|
|
56
|
-
constraint
|
|
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 (
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
to public
|
|
70
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
for
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
14
|
-
|
|
27
|
+
username: body.username,
|
|
28
|
+
display_name: body.display_name,
|
|
15
29
|
},
|
|
16
30
|
},
|
|
17
31
|
})
|
|
18
32
|
|
|
19
33
|
if (error) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
cause: error.cause,
|
|
34
|
+
return {
|
|
35
|
+
success: false,
|
|
23
36
|
message: error.message,
|
|
24
|
-
|
|
37
|
+
error,
|
|
38
|
+
}
|
|
25
39
|
}
|
|
26
40
|
|
|
27
|
-
return {
|
|
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
|
+
}
|