@plutocms/supabase 0.0.1-alpha.9 → 0.1.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.
- package/CHANGELOG.md +105 -0
- package/README.md +2 -4
- package/app/assets/css/tailwind.css +5 -0
- package/app/components/navbar/NavbarAdmin.vue +6 -5
- package/app/composables/auth.ts +124 -2
- package/app/middleware/auth.ts +9 -6
- package/app/middleware/setup-check.ts +25 -21
- package/app/pages/admin/(auth)/confirm.vue +38 -0
- package/app/pages/admin/(auth)/forgot-password.vue +88 -0
- package/app/pages/admin/(auth)/login.vue +5 -1
- package/app/pages/admin/(auth)/signup.vue +76 -16
- package/app/pages/admin/(auth)/update-password.vue +134 -0
- package/app/pages/admin/setup.vue +5 -142
- package/app/pages/admin.vue +2 -4
- package/modules/pluto-migrations.ts +121 -0
- package/nuxt.config.ts +14 -1
- package/package.json +40 -24
- package/public/schema.sql +46 -5
- package/scripts/supabase-typegen.ts +36 -21
- package/scripts/tsconfig.json +18 -0
- package/server/api/{signup.post.ts → auth/signup.post.ts} +7 -16
- package/server/api/setup/create.post.ts +39 -82
- package/server/plugins/migrations.ts +60 -0
- package/server/utils/migrations.ts +88 -0
- package/server/utils/sql.ts +81 -0
- package/server/utils/typegen.ts +68 -0
- package/shared/types/runtime-config.d.ts +16 -0
- package/shared/types/supabase.ts +138 -124
- package/.agents/skills/nuxt/GENERATION.md +0 -5
- package/.agents/skills/nuxt/SKILL.md +0 -55
- package/.agents/skills/nuxt/references/advanced-hooks.md +0 -289
- package/.agents/skills/nuxt/references/advanced-layers.md +0 -299
- package/.agents/skills/nuxt/references/advanced-module-authoring.md +0 -554
- package/.agents/skills/nuxt/references/best-practices-data-fetching.md +0 -357
- package/.agents/skills/nuxt/references/best-practices-ssr.md +0 -355
- package/.agents/skills/nuxt/references/core-cli.md +0 -263
- package/.agents/skills/nuxt/references/core-config.md +0 -162
- package/.agents/skills/nuxt/references/core-data-fetching.md +0 -236
- package/.agents/skills/nuxt/references/core-deployment.md +0 -224
- package/.agents/skills/nuxt/references/core-directory-structure.md +0 -269
- package/.agents/skills/nuxt/references/core-modules.md +0 -292
- package/.agents/skills/nuxt/references/core-routing.md +0 -226
- package/.agents/skills/nuxt/references/features-components-autoimport.md +0 -328
- package/.agents/skills/nuxt/references/features-components.md +0 -264
- package/.agents/skills/nuxt/references/features-composables.md +0 -276
- package/.agents/skills/nuxt/references/features-server.md +0 -265
- package/.agents/skills/nuxt/references/features-state.md +0 -194
- package/.agents/skills/nuxt/references/rendering-modes.md +0 -237
- package/.agents/skills/nuxt-ui/SKILL.md +0 -334
- package/.agents/skills/nuxt-ui/references/components.md +0 -377
- package/.agents/skills/nuxt-ui/references/composables.md +0 -127
- package/.agents/skills/nuxt-ui/references/layouts/chat.md +0 -195
- package/.agents/skills/nuxt-ui/references/layouts/dashboard.md +0 -220
- package/.agents/skills/nuxt-ui/references/layouts/docs.md +0 -141
- package/.agents/skills/nuxt-ui/references/layouts/editor.md +0 -167
- package/.agents/skills/nuxt-ui/references/layouts/page.md +0 -260
- package/.agents/skills/nuxt-ui/references/theming.md +0 -427
- package/.agents/skills/supabase-postgres-best-practices/AGENTS.md +0 -68
- package/.agents/skills/supabase-postgres-best-practices/CLAUDE.md +0 -1
- package/.agents/skills/supabase-postgres-best-practices/README.md +0 -116
- package/.agents/skills/supabase-postgres-best-practices/SKILL.md +0 -64
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +0 -49
- package/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +0 -46
- package/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +0 -44
- package/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +0 -41
- package/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +0 -46
- package/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +0 -54
- package/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +0 -53
- package/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +0 -56
- package/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +0 -68
- package/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +0 -54
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +0 -45
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +0 -44
- package/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +0 -40
- package/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +0 -48
- package/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +0 -43
- package/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +0 -45
- package/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +0 -80
- package/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +0 -46
- package/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +0 -59
- package/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +0 -61
- package/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +0 -54
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +0 -57
- package/.editorconfig +0 -12
- package/.eslintignore +0 -1
- package/.prettierrc +0 -9
- package/.vscode/extensions.json +0 -10
- package/.vscode/settings.json +0 -61
- package/bun.lock +0 -2837
- package/eslint.config.mjs +0 -88
- package/server/api/category/list.get.ts +0 -9
- package/server/api/category/new.post.ts +0 -31
- package/skills-lock.json +0 -20
package/nuxt.config.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
// Point these at a local checkout (e.g. `../pluto`, `../utils`) to test
|
|
2
|
+
// unpublished changes; unset, they resolve to the published npm packages.
|
|
3
|
+
const plutoLayer = process.env.PLUTO_PLUTO_PATH || '@plutocms/pluto'
|
|
4
|
+
const utilsLayer = process.env.PLUTO_UTILS_PATH || '@plutocms/utils'
|
|
5
|
+
|
|
1
6
|
export default defineNuxtConfig({
|
|
2
|
-
extends: [[
|
|
7
|
+
extends: [[plutoLayer, { install: true }], utilsLayer],
|
|
3
8
|
|
|
4
9
|
modules: ['@nuxt/eslint', '@nuxtjs/supabase'],
|
|
5
10
|
|
|
@@ -10,6 +15,8 @@ export default defineNuxtConfig({
|
|
|
10
15
|
runtimeConfig: {
|
|
11
16
|
supabaseUrl: process.env.SUPABASE_URL,
|
|
12
17
|
supabaseKey: process.env.SUPABASE_KEY,
|
|
18
|
+
plutoRootDir: '',
|
|
19
|
+
plutoLayerSchemas: {} as Record<string, string>,
|
|
13
20
|
},
|
|
14
21
|
|
|
15
22
|
alias: {
|
|
@@ -24,6 +31,12 @@ export default defineNuxtConfig({
|
|
|
24
31
|
},
|
|
25
32
|
},
|
|
26
33
|
|
|
34
|
+
vite: {
|
|
35
|
+
optimizeDeps: {
|
|
36
|
+
include: ['yup'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
27
40
|
eslint: {
|
|
28
41
|
config: {
|
|
29
42
|
nuxt: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"trustedDependencies": [
|
|
6
|
+
"@parcel/watcher",
|
|
7
|
+
"@plutocms/pluto",
|
|
8
|
+
"@plutocms/utils",
|
|
9
|
+
"esbuild",
|
|
10
|
+
"supabase",
|
|
11
|
+
"unrs-resolver",
|
|
12
|
+
"vue-demi"
|
|
13
|
+
],
|
|
14
|
+
"description": "Supabase backend layer for Pluto CMS",
|
|
15
|
+
"author": "Victor Ribeiro",
|
|
16
|
+
"homepage": "https://github.com/plutocms/supabase#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/plutocms/supabase.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/plutocms/supabase/issues"
|
|
23
|
+
},
|
|
5
24
|
"main": "./nuxt.config.ts",
|
|
6
25
|
"scripts": {
|
|
7
26
|
"build": "nuxt build",
|
|
@@ -12,35 +31,32 @@
|
|
|
12
31
|
"prettier": "prettier --write .",
|
|
13
32
|
"lint": "eslint .",
|
|
14
33
|
"lint:fix": "eslint . --fix && prettier --write .",
|
|
34
|
+
"typecheck": "nuxi typecheck",
|
|
15
35
|
"supabase-types": "bun ./scripts/supabase-typegen.ts"
|
|
16
36
|
},
|
|
17
37
|
"dependencies": {
|
|
18
|
-
"@
|
|
19
|
-
"@
|
|
20
|
-
"@plutocms/
|
|
21
|
-
"
|
|
22
|
-
"
|
|
38
|
+
"@nuxt/ui": "^4.9.0",
|
|
39
|
+
"@nuxtjs/supabase": "^2.0.9",
|
|
40
|
+
"@plutocms/pluto": "^0.1.0",
|
|
41
|
+
"@plutocms/utils": "^0.1.0",
|
|
42
|
+
"postgres": "^3.4.9",
|
|
43
|
+
"supabase": "^2.109.1",
|
|
23
44
|
"yup": "1.7.1"
|
|
24
45
|
},
|
|
25
46
|
"devDependencies": {
|
|
26
47
|
"@antfu/eslint-config": "^6.2.0",
|
|
27
|
-
"@nuxt/eslint": "^1.
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"vue": "^3.5.
|
|
35
|
-
"vue-router": "^5.0
|
|
48
|
+
"@nuxt/eslint": "^1.16.0",
|
|
49
|
+
"@types/bun": "^1.3.14",
|
|
50
|
+
"@vueuse/nuxt": "^14.3.0",
|
|
51
|
+
"eslint": "^10.6.0",
|
|
52
|
+
"nuxt": "^4.4.2",
|
|
53
|
+
"prettier": "^3.9.4",
|
|
54
|
+
"tailwindcss": "^4.3.2",
|
|
55
|
+
"vue": "^3.5.39",
|
|
56
|
+
"vue-router": "^5.1.0",
|
|
57
|
+
"vue-tsc": "^3.3.7"
|
|
36
58
|
},
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
"@plutocms/utils",
|
|
41
|
-
"esbuild",
|
|
42
|
-
"supabase",
|
|
43
|
-
"unrs-resolver",
|
|
44
|
-
"vue-demi"
|
|
45
|
-
]
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
46
62
|
}
|
package/public/schema.sql
CHANGED
|
@@ -106,12 +106,12 @@ alter table public.healthcheck enable row level security;
|
|
|
106
106
|
do $$
|
|
107
107
|
begin
|
|
108
108
|
if not exists (
|
|
109
|
-
select 1 from pg_policies where tablename = 'healthcheck' and policyname = 'Enable read access for healthcheck to public
|
|
109
|
+
select 1 from pg_policies where tablename = 'healthcheck' and policyname = 'Enable read access for healthcheck to public'
|
|
110
110
|
) then
|
|
111
|
-
create policy "Enable read access for healthcheck to public
|
|
111
|
+
create policy "Enable read access for healthcheck to public"
|
|
112
112
|
on public.healthcheck
|
|
113
113
|
for select
|
|
114
|
-
to public
|
|
114
|
+
to public
|
|
115
115
|
using (true);
|
|
116
116
|
end if;
|
|
117
117
|
end
|
|
@@ -137,6 +137,7 @@ $$;
|
|
|
137
137
|
create table if not exists public.profiles (
|
|
138
138
|
id uuid references auth.users on delete cascade not null primary key,
|
|
139
139
|
updated_at timestamp with time zone,
|
|
140
|
+
email text,
|
|
140
141
|
username text,
|
|
141
142
|
display_name text,
|
|
142
143
|
is_admin boolean not null default false,
|
|
@@ -160,14 +161,29 @@ end
|
|
|
160
161
|
$$;
|
|
161
162
|
|
|
162
163
|
--- inserts a row into public.profiles
|
|
164
|
+
--- automatically grants admin to the first user if no profiles exist yet
|
|
163
165
|
create or replace function public.handle_new_user()
|
|
164
166
|
returns trigger
|
|
165
167
|
language plpgsql
|
|
166
168
|
security definer set search_path = ''
|
|
167
169
|
as $$
|
|
170
|
+
declare
|
|
171
|
+
profile_count int;
|
|
172
|
+
should_be_admin boolean;
|
|
168
173
|
begin
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
);
|
|
171
187
|
return new;
|
|
172
188
|
end;
|
|
173
189
|
$$;
|
|
@@ -183,3 +199,28 @@ begin
|
|
|
183
199
|
end if;
|
|
184
200
|
end
|
|
185
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
|
+
$$;
|
|
@@ -1,35 +1,50 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
1
|
import { cwd } from 'node:process'
|
|
4
|
-
import { $ } from 'bun'
|
|
5
2
|
import { consola } from 'consola'
|
|
3
|
+
import { regenerateSupabaseTypes } from '../server/utils/typegen'
|
|
6
4
|
|
|
5
|
+
const databaseUrl = process.env.DATABASE_URL
|
|
7
6
|
const projectUrl = process.env.SUPABASE_URL
|
|
8
|
-
const projectId = projectUrl?.split('.supabase.co')[0]?.replace('https://', '')
|
|
9
|
-
const _path = path
|
|
10
|
-
.resolve(cwd(), './shared/types/supabase.ts')
|
|
11
|
-
.replace(/\\/g, '/')
|
|
12
7
|
|
|
13
8
|
;(async () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
if (databaseUrl) {
|
|
10
|
+
consola.info('Generating Supabase types via DATABASE_URL...')
|
|
11
|
+
await regenerateSupabaseTypes(cwd(), databaseUrl)
|
|
12
|
+
return
|
|
13
|
+
}
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
if (!projectUrl) {
|
|
16
|
+
consola.error(
|
|
17
|
+
'Cannot generate Supabase types: neither DATABASE_URL nor SUPABASE_URL is set.'
|
|
18
|
+
)
|
|
19
|
+
return
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
consola.warn(
|
|
23
|
+
'DATABASE_URL is not set; falling back to --project-id (requires `supabase login`).'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const projectId = projectUrl.split('.supabase.co')[0]?.replace('https://', '')
|
|
27
|
+
const { execFile } = await import('node:child_process')
|
|
28
|
+
const { promisify } = await import('node:util')
|
|
29
|
+
const { writeFileSync } = await import('node:fs')
|
|
30
|
+
const { join } = await import('node:path')
|
|
31
|
+
|
|
32
|
+
consola.info(`Generating Supabase types for project: ${projectId}`)
|
|
30
33
|
|
|
31
34
|
try {
|
|
32
|
-
|
|
35
|
+
const { stdout } = await promisify(execFile)(
|
|
36
|
+
'supabase',
|
|
37
|
+
[
|
|
38
|
+
'gen',
|
|
39
|
+
'types',
|
|
40
|
+
'typescript',
|
|
41
|
+
`--project-id=${projectId}`,
|
|
42
|
+
'--schema=public',
|
|
43
|
+
],
|
|
44
|
+
{ maxBuffer: 1024 * 1024 * 16 }
|
|
45
|
+
)
|
|
46
|
+
writeFileSync(join(cwd(), 'shared/types/supabase.ts'), stdout, 'utf-8')
|
|
47
|
+
consola.success('Supabase types generated.')
|
|
33
48
|
} catch (error) {
|
|
34
49
|
consola.error('Unhandled error:', error)
|
|
35
50
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
// This folder is excluded from the Nuxt app itself (see `ignore` in
|
|
3
|
+
// ../nuxt.config.ts), so it needs its own project - otherwise editors have
|
|
4
|
+
// no tsconfig to resolve against here and Node globals (process, fs, etc.)
|
|
5
|
+
// won't show up.
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"target": "ESNext",
|
|
8
|
+
"lib": ["ESNext"],
|
|
9
|
+
"moduleDetection": "force",
|
|
10
|
+
"module": "ESNext",
|
|
11
|
+
"moduleResolution": "Bundler",
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"skipLibCheck": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["./**/*.ts", "../server/utils/typegen.ts"]
|
|
18
|
+
}
|
|
@@ -5,7 +5,6 @@ interface Payload {
|
|
|
5
5
|
display_name: string
|
|
6
6
|
email: string
|
|
7
7
|
password: string
|
|
8
|
-
is_admin?: boolean
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export default defineEventHandler(async (event) => {
|
|
@@ -13,6 +12,13 @@ export default defineEventHandler(async (event) => {
|
|
|
13
12
|
|
|
14
13
|
const body = await readBody<Payload>(event)
|
|
15
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
|
+
}
|
|
21
|
+
|
|
16
22
|
const { data, error } = await client.auth.signUp({
|
|
17
23
|
email: body.email,
|
|
18
24
|
password: body.password,
|
|
@@ -20,7 +26,6 @@ export default defineEventHandler(async (event) => {
|
|
|
20
26
|
data: {
|
|
21
27
|
username: body.username,
|
|
22
28
|
display_name: body.display_name,
|
|
23
|
-
is_admin: body.is_admin || false,
|
|
24
29
|
},
|
|
25
30
|
},
|
|
26
31
|
})
|
|
@@ -33,20 +38,6 @@ export default defineEventHandler(async (event) => {
|
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
// Set the first_setup flag in the healthcheck table to false
|
|
37
|
-
const { error: healthcheckError } = await client
|
|
38
|
-
.from('healthcheck')
|
|
39
|
-
.update({ config_value: 'false' })
|
|
40
|
-
.eq('config_name', 'first_setup')
|
|
41
|
-
|
|
42
|
-
if (healthcheckError) {
|
|
43
|
-
return {
|
|
44
|
-
success: false,
|
|
45
|
-
message: healthcheckError.message,
|
|
46
|
-
error: healthcheckError,
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
41
|
return {
|
|
51
42
|
success: true,
|
|
52
43
|
message: 'User registered successfully',
|
|
@@ -1,92 +1,13 @@
|
|
|
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
|
|
5
8
|
connectionString: string
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
/**
|
|
9
|
-
* Splits a SQL string into individual statements, correctly handling
|
|
10
|
-
* $$ dollar-quoted blocks, single-quoted strings, and -- comments.
|
|
11
|
-
*/
|
|
12
|
-
function splitStatements(sql: string): string[] {
|
|
13
|
-
const statements: string[] = []
|
|
14
|
-
let current = ''
|
|
15
|
-
let i = 0
|
|
16
|
-
|
|
17
|
-
while (i < sql.length) {
|
|
18
|
-
// Check for dollar-quoting ($$)
|
|
19
|
-
if (sql[i] === '$' && sql[i + 1] === '$') {
|
|
20
|
-
current += '$$'
|
|
21
|
-
i += 2
|
|
22
|
-
// Read until closing $$
|
|
23
|
-
while (i < sql.length) {
|
|
24
|
-
if (sql[i] === '$' && sql[i + 1] === '$') {
|
|
25
|
-
current += '$$'
|
|
26
|
-
i += 2
|
|
27
|
-
break
|
|
28
|
-
}
|
|
29
|
-
current += sql[i]
|
|
30
|
-
i++
|
|
31
|
-
}
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Check for single-quoted strings (handle '' escapes)
|
|
36
|
-
if (sql[i] === `'`) {
|
|
37
|
-
current += sql[i]
|
|
38
|
-
i++
|
|
39
|
-
while (i < sql.length) {
|
|
40
|
-
if (sql[i] === `'` && sql[i + 1] === `'`) {
|
|
41
|
-
// Escaped quote
|
|
42
|
-
current += `''`
|
|
43
|
-
i += 2
|
|
44
|
-
continue
|
|
45
|
-
}
|
|
46
|
-
if (sql[i] === `'`) {
|
|
47
|
-
current += sql[i]
|
|
48
|
-
i++
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
current += sql[i]
|
|
52
|
-
i++
|
|
53
|
-
}
|
|
54
|
-
continue
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check for single-line comments
|
|
58
|
-
if (sql[i] === '-' && sql[i + 1] === '-') {
|
|
59
|
-
while (i < sql.length && sql[i] !== '\n') {
|
|
60
|
-
current += sql[i]
|
|
61
|
-
i++
|
|
62
|
-
}
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Statement terminator
|
|
67
|
-
if (sql[i] === ';') {
|
|
68
|
-
current += ';'
|
|
69
|
-
const trimmed = current.trim()
|
|
70
|
-
if (trimmed && trimmed !== ';') {
|
|
71
|
-
statements.push(trimmed)
|
|
72
|
-
}
|
|
73
|
-
current = ''
|
|
74
|
-
i++
|
|
75
|
-
continue
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
current += sql[i]
|
|
79
|
-
i++
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const trimmed = current.trim()
|
|
83
|
-
if (trimmed && trimmed !== ';') {
|
|
84
|
-
statements.push(trimmed)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return statements
|
|
88
|
-
}
|
|
89
|
-
|
|
90
11
|
export default defineEventHandler(async (event) => {
|
|
91
12
|
const body = await readBody<Payload>(event)
|
|
92
13
|
|
|
@@ -118,6 +39,42 @@ export default defineEventHandler(async (event) => {
|
|
|
118
39
|
await sql.unsafe(statement)
|
|
119
40
|
}
|
|
120
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')
|
|
77
|
+
|
|
121
78
|
return {
|
|
122
79
|
success: true,
|
|
123
80
|
message: 'Database setup completed successfully.',
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { runLayerMigration } from '../utils/migrations'
|
|
2
|
+
import { regenerateSupabaseTypes } from '../utils/typegen'
|
|
3
|
+
|
|
4
|
+
export default defineNitroPlugin(async () => {
|
|
5
|
+
// Only run if DATABASE_URL is configured (setup already completed)
|
|
6
|
+
if (!process.env.DATABASE_URL) {
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const config = useRuntimeConfig()
|
|
11
|
+
const layerSchemas = config.plutoLayerSchemas ?? {}
|
|
12
|
+
|
|
13
|
+
const layerNames = Object.keys(layerSchemas)
|
|
14
|
+
|
|
15
|
+
if (layerNames.length === 0) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let anyApplied = false
|
|
20
|
+
|
|
21
|
+
for (const layerName of layerNames) {
|
|
22
|
+
const schemaSql = layerSchemas[layerName]
|
|
23
|
+
|
|
24
|
+
if (!schemaSql) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const result = await runLayerMigration({
|
|
30
|
+
layerName,
|
|
31
|
+
schemaSql,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (result.skipped) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[migrations] Layer "${layerName}" already applied, skipped.`
|
|
37
|
+
)
|
|
38
|
+
} else if (result.success) {
|
|
39
|
+
console.warn(`[migrations] Layer "${layerName}" migrated successfully.`)
|
|
40
|
+
anyApplied = true
|
|
41
|
+
} else {
|
|
42
|
+
console.error(`[migrations] Layer "${layerName}" failed:`, result.error)
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(
|
|
46
|
+
`[migrations] Error running migration for "${layerName}":`,
|
|
47
|
+
error
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Regenerate Database types from the live schema whenever a layer schema
|
|
53
|
+
// was newly applied, so `Database` stays in sync without a manual step.
|
|
54
|
+
// Re-editing an already-applied layer schema is a no-op here (it's
|
|
55
|
+
// recorded as `skipped`); use the `supabase-types` script to force a
|
|
56
|
+
// refresh in that case.
|
|
57
|
+
if (import.meta.dev && anyApplied && config.plutoRootDir) {
|
|
58
|
+
await regenerateSupabaseTypes(config.plutoRootDir)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
@@ -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
|
+
}
|