sveltekit-auth-example 5.0.3 → 5.1.0

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/.yarnrc.yml CHANGED
@@ -1 +1,3 @@
1
1
  nodeLinker: node-modules
2
+
3
+ yarnPath: .yarn/releases/yarn-4.13.0.cjs
package/AGENTS.md ADDED
@@ -0,0 +1,23 @@
1
+ You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
2
+
3
+ ## Available MCP Tools:
4
+
5
+ ### 1. list-sections
6
+
7
+ Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
8
+ When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
9
+
10
+ ### 2. get-documentation
11
+
12
+ Retrieves full documentation content for specific sections. Accepts single or multiple sections.
13
+ After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
14
+
15
+ ### 3. svelte-autofixer
16
+
17
+ Analyzes Svelte code and returns issues and suggestions.
18
+ You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
19
+
20
+ ### 4. playground-link
21
+
22
+ Generates a Svelte Playground link with the provided code.
23
+ After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
package/CHANGELOG.md CHANGED
@@ -2,13 +2,24 @@
2
2
 
3
3
  - Add password complexity checking on /register and /profile pages (only checks for length currently despite what the pages say)
4
4
 
5
+ # 5.1.0
6
+
7
+ - Convert to Tailwind CSS
8
+
9
+ # 5.0.4
10
+
11
+ - Bump dependencies
12
+
5
13
  # 5.0.3
14
+
6
15
  - Bump dependencies
7
16
 
8
17
  # 5.0.2
18
+
9
19
  - Bump dependencies
10
20
 
11
21
  # 5.0.1
22
+
12
23
  - Migrate to Svelte 5 runes mode
13
24
  - Format with prettier
14
25
  - Bump dependencies
package/README.md CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  **Updated for Svelte 5 and SvelteKit 2.19**
4
4
 
5
- This is an example of how to register, authenticate, and update users and limit their access to
6
- areas of the website by role (admin, teacher, student). It includes profile management and password resets via SendGrid.
5
+ This is an example of how to register, authenticate, and update users and limit their access to areas of the website by role (admin, teacher, student). It includes profile management and password resets via SendGrid.
7
6
 
8
- It's a Single Page App (SPA) built with SvelteKit and a PostgreSQL database back-end. Code is TypeScript and the website is styled using Bootstrap. PostgreSQL functions handle password hashing and UUID generation for the session ID. Unlike most authentication examples, this SPA does not use callbacks that redirect back to the site (causing the website to be reloaded with a visual flash).
7
+ It's a Single Page App (SPA) built with SvelteKit and a PostgreSQL database back-end. Code is TypeScript and the website is styled using Tailwind CSS. PostgreSQL functions handle password hashing and UUID generation for the session ID. Unlike most authentication examples, this SPA does not use callbacks that redirect back to the site (causing the website to be reloaded with a visual flash).
9
8
 
10
9
  The project includes a Content Security Policy (CSP) in svelte.config.js.
11
10
 
package/db_create.sql CHANGED
@@ -26,7 +26,6 @@ CREATE DATABASE auth
26
26
  WITH
27
27
  OWNER = auth
28
28
  ENCODING = 'UTF8'
29
- TABLESPACE = pg_default
30
29
  CONNECTION LIMIT = -1;
31
30
 
32
31
  COMMENT ON DATABASE auth IS 'SvelteKit Auth Example';
@@ -40,20 +39,44 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
40
39
  -- Required to generate UUIDs for sessions
41
40
  CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
42
41
 
42
+ -- Required for case-insensitive text (email_address domain)
43
+ CREATE EXTENSION IF NOT EXISTS citext;
44
+
43
45
  -- Using hard-coded roles (often this would be a table)
44
46
  CREATE TYPE public.roles AS ENUM
45
47
  ('student', 'teacher', 'admin');
46
48
 
47
49
  ALTER TYPE public.roles OWNER TO auth;
48
50
 
51
+ -- Domains
52
+ CREATE DOMAIN public.email_address AS citext CHECK (
53
+ length(VALUE) <= 254
54
+ AND VALUE = btrim(VALUE)
55
+ AND VALUE ~* '^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$'
56
+ );
57
+
58
+ COMMENT ON DOMAIN public.email_address IS 'RFC-compliant email address (case-insensitive, max 254 chars)';
59
+
60
+ CREATE DOMAIN public.persons_name AS text CHECK (length(VALUE) <= 20) NOT NULL;
61
+
62
+ COMMENT ON DOMAIN public.persons_name IS 'Person first or last name (max 20 characters)';
63
+
64
+ CREATE DOMAIN public.phone_number AS text CHECK (
65
+ VALUE IS NULL
66
+ OR length(VALUE) <= 50
67
+ );
68
+
69
+ COMMENT ON DOMAIN public.phone_number IS 'Phone number (max 50 characters)';
70
+
49
71
  CREATE TABLE IF NOT EXISTS public.users (
50
72
  id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
51
73
  role roles NOT NULL DEFAULT 'student'::roles,
52
- email character varying(80) COLLATE pg_catalog."default" NOT NULL,
74
+ email email_address NOT NULL,
53
75
  password character varying(80) COLLATE pg_catalog."default",
54
- first_name character varying(20) COLLATE pg_catalog."default" NOT NULL,
55
- last_name character varying(20) COLLATE pg_catalog."default" NOT NULL,
56
- phone character varying(23) COLLATE pg_catalog."default",
76
+ first_name persons_name,
77
+ last_name persons_name,
78
+ opt_out boolean NOT NULL DEFAULT false,
79
+ phone phone_number,
57
80
  CONSTRAINT users_pkey PRIMARY KEY (id),
58
81
  CONSTRAINT users_email_unique UNIQUE (email)
59
82
  ) TABLESPACE pg_default;
@@ -78,13 +101,13 @@ CREATE INDEX users_password
78
101
  CREATE TABLE IF NOT EXISTS public.sessions (
79
102
  id uuid NOT NULL DEFAULT uuid_generate_v4(),
80
103
  user_id integer NOT NULL,
81
- expires timestamp with time zone DEFAULT (CURRENT_TIMESTAMP + '02:00:00'::interval),
104
+ expires timestamptz NOT NULL DEFAULT (CURRENT_TIMESTAMP + INTERVAL '2 hours'),
82
105
  CONSTRAINT sessions_pkey PRIMARY KEY (id),
83
106
  CONSTRAINT sessions_user_fkey FOREIGN KEY (user_id)
84
107
  REFERENCES public.users (id) MATCH SIMPLE
85
- ON UPDATE NO ACTION
86
- ON DELETE CASCADE
87
- NOT VALID
108
+ ON UPDATE CASCADE
109
+ ON DELETE CASCADE,
110
+ CONSTRAINT sessions_one_per_user UNIQUE (user_id)
88
111
  ) TABLESPACE pg_default;
89
112
 
90
113
  ALTER TABLE public.sessions OWNER to auth;
@@ -98,8 +121,8 @@ CREATE OR REPLACE FUNCTION public.authenticate(
98
121
  VOLATILE PARALLEL UNSAFE
99
122
  AS $BODY$
100
123
  DECLARE
101
- input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
102
- input_password varchar(80) := (input->>'password')::varchar;
124
+ input_email text := trim(input->>'email');
125
+ input_password text := input->>'password';
103
126
  BEGIN
104
127
  IF input_email IS NULL OR input_password IS NULL THEN
105
128
  response := json_build_object('statusCode', 400, 'status', 'Please provide an email address and password to authenticate.', 'user', NULL);
@@ -107,24 +130,25 @@ BEGIN
107
130
  END IF;
108
131
 
109
132
  WITH user_authenticated AS (
110
- SELECT users.id, role, first_name, last_name, phone
133
+ SELECT users.id, role, first_name, last_name, phone, opt_out
111
134
  FROM users
112
135
  WHERE email = input_email AND password = crypt(input_password, password) LIMIT 1
113
136
  )
114
137
  SELECT json_build_object(
115
- 'statusCode', CASE WHEN (SELECT COUNT(*) FROM user_authenticated) > 0 THEN 200 ELSE 401 END,
116
- 'status', CASE WHEN (SELECT COUNT(*) FROM user_authenticated) > 0
138
+ 'statusCode', CASE WHEN EXISTS (SELECT 1 FROM user_authenticated) THEN 200 ELSE 401 END,
139
+ 'status', CASE WHEN EXISTS (SELECT 1 FROM user_authenticated)
117
140
  THEN 'Login successful.'
118
141
  ELSE 'Invalid username/password combination.'
119
142
  END,
120
- 'user', CASE WHEN (SELECT COUNT(*) FROM user_authenticated) > 0
143
+ 'user', CASE WHEN EXISTS (SELECT 1 FROM user_authenticated)
121
144
  THEN (SELECT json_build_object(
122
145
  'id', user_authenticated.id,
123
146
  'role', user_authenticated.role,
124
147
  'email', input_email,
125
148
  'firstName', user_authenticated.first_name,
126
149
  'lastName', user_authenticated.last_name,
127
- 'phone', user_authenticated.phone)
150
+ 'phone', user_authenticated.phone,
151
+ 'optOut', user_authenticated.opt_out)
128
152
  FROM user_authenticated)
129
153
  ELSE NULL
130
154
  END,
@@ -142,8 +166,12 @@ CREATE OR REPLACE FUNCTION public.create_session(
142
166
  COST 100
143
167
  VOLATILE PARALLEL UNSAFE
144
168
  AS $BODY$
145
- DELETE FROM sessions WHERE user_id = input_user_id;
146
- INSERT INTO sessions(user_id) VALUES (input_user_id) RETURNING sessions.id;
169
+ -- Remove expired sessions (index-friendly cleanup)
170
+ DELETE FROM sessions WHERE expires < CURRENT_TIMESTAMP;
171
+ -- Remove any existing session(s) for this user
172
+ DELETE FROM sessions WHERE user_id = input_user_id;
173
+ -- Create the new session
174
+ INSERT INTO sessions(user_id) VALUES (input_user_id) RETURNING sessions.id;
147
175
  $BODY$;
148
176
 
149
177
  ALTER FUNCTION public.create_session(integer) OWNER TO auth;
@@ -159,6 +187,7 @@ SELECT json_build_object(
159
187
  'firstName', users.first_name,
160
188
  'lastName', users.last_name,
161
189
  'phone', users.phone,
190
+ 'optOut', users.opt_out,
162
191
  'expires', sessions.expires
163
192
  ) AS user
164
193
  FROM sessions
@@ -177,11 +206,11 @@ CREATE OR REPLACE FUNCTION public.register(
177
206
  VOLATILE PARALLEL UNSAFE
178
207
  AS $BODY$
179
208
  DECLARE
180
- input_email varchar(80) := LOWER(TRIM((input->>'email')::varchar));
181
- input_first_name varchar(20) := TRIM((input->>'firstName')::varchar);
182
- input_last_name varchar(20) := TRIM((input->>'lastName')::varchar);
183
- input_phone varchar(23) := TRIM((input->>'phone')::varchar);
184
- input_password varchar(80) := (input->>'password')::varchar;
209
+ input_email text := trim(input->>'email');
210
+ input_first_name text := trim(input->>'firstName');
211
+ input_last_name text := trim(input->>'lastName');
212
+ input_phone text := trim(input->>'phone');
213
+ input_password text := input->>'password';
185
214
  BEGIN
186
215
  PERFORM id FROM users WHERE email = input_email;
187
216
  IF NOT FOUND THEN
@@ -190,7 +219,7 @@ BEGIN
190
219
  RETURNING
191
220
  json_build_object(
192
221
  'sessionId', create_session(users.id),
193
- 'user', json_build_object('id', users.id, 'role', 'student', 'email', input_email, 'firstName', input_first_name, 'lastName', input_last_name, 'phone', input_phone, 'optOut', false)
222
+ 'user', json_build_object('id', users.id, 'role', 'student', 'email', input_email, 'firstName', input_first_name, 'lastName', input_last_name, 'phone', input_phone, 'optOut', users.opt_out)
194
223
  ) INTO user_session;
195
224
  ELSE -- user is registering account that already exists so set sessionId and user to null so client can let them know
196
225
  SELECT authenticate(input) INTO user_session;
@@ -0,0 +1,48 @@
1
+ import prettier from 'eslint-config-prettier'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { includeIgnoreFile } from '@eslint/compat'
4
+ import js from '@eslint/js'
5
+ import svelte from 'eslint-plugin-svelte'
6
+ import { defineConfig } from 'eslint/config'
7
+ import globals from 'globals'
8
+ import ts from 'typescript-eslint'
9
+ import svelteConfig from './svelte.config.js'
10
+
11
+ const prettierIgnorePath = fileURLToPath(new URL('./.prettierignore', import.meta.url))
12
+
13
+ export default defineConfig(
14
+ includeIgnoreFile(prettierIgnorePath),
15
+ js.configs.recommended,
16
+ ...ts.configs.recommended,
17
+ ...svelte.configs.recommended,
18
+ prettier,
19
+ ...svelte.configs.prettier,
20
+ {
21
+ languageOptions: {
22
+ globals: {
23
+ ...globals.browser,
24
+ ...globals.node
25
+ }
26
+ },
27
+ rules: {
28
+ // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
29
+ // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
30
+ 'no-undef': 'off'
31
+ }
32
+ },
33
+ {
34
+ files: ['**/*.svelte', '**/*.svelte.ts'],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ projectService: true,
38
+ extraFileExtensions: ['.svelte'],
39
+ parser: ts.parser,
40
+ svelteConfig
41
+ }
42
+ },
43
+ rules: {
44
+ 'svelte/no-at-html-tags': 'warn',
45
+ 'svelte/require-each-key': 'warn'
46
+ }
47
+ }
48
+ )
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sveltekit-auth-example",
3
3
  "description": "SvelteKit Authentication Example",
4
- "version": "5.0.3",
4
+ "version": "5.1.0",
5
5
  "author": "Nate Stuyvesant",
6
6
  "license": "https://github.com/nstuyvesant/sveltekit-auth-example/blob/master/LICENSE",
7
7
  "repository": {
@@ -20,52 +20,55 @@
20
20
  "postgresql"
21
21
  ],
22
22
  "scripts": {
23
- "start": "node build",
24
23
  "dev": "vite dev",
24
+ "prepare": "svelte-kit sync || echo ''",
25
25
  "build": "vite build",
26
26
  "package": "svelte-kit package",
27
- "preview": "vite preview",
27
+ "preview": "vite preview --port 3000 --open",
28
28
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
29
29
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
30
- "lint": "prettier --check . && eslint . --ignore-path ./.eslintignore",
31
- "format": "prettier --write . --ignore-path ./.eslintignore"
30
+ "lint": "prettier --check . && eslint .",
31
+ "format": "prettier --write ."
32
32
  },
33
33
  "engines": {
34
- "node": "^20.18.0"
34
+ "node": "^24.14.0"
35
35
  },
36
36
  "type": "module",
37
37
  "dependencies": {
38
- "@sendgrid/mail": "^8.1.4",
39
- "pg": "^8.14.1"
38
+ "@sendgrid/mail": "^8.1.6",
39
+ "google-auth-library": "^10.6.1",
40
+ "pg": "^8.20.0"
40
41
  },
41
42
  "devDependencies": {
42
- "@eslint/js": "^9.23.0",
43
- "@sveltejs/adapter-node": "^5.2.12",
44
- "@sveltejs/kit": "^2.20.4",
45
- "@sveltejs/vite-plugin-svelte": "^5.0.3",
43
+ "@eslint/js": "^10.0.1",
44
+ "@playwright/test": "^1.58.2",
45
+ "@sveltejs/adapter-node": "^5.5.4",
46
+ "@sveltejs/kit": "^2.54.0",
47
+ "@sveltejs/mcp": "^0.1.21",
48
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
49
+ "@tailwindcss/vite": "^4.2.1",
46
50
  "@types/bootstrap": "5.2.10",
47
- "@types/google.accounts": "^0.0.15",
48
- "@types/jsonwebtoken": "^9.0.9",
49
- "@types/pg": "^8.11.11",
50
- "bootstrap": "^5.3.4",
51
- "eslint": "^9.23.0",
52
- "eslint-config-prettier": "^10.1.1",
53
- "eslint-plugin-prettier": "^5.2.6",
54
- "eslint-plugin-svelte": "^3.5.1",
55
- "globals": "^16.0.0",
56
- "google-auth-library": "^9.15.1",
57
- "jsonwebtoken": "^9.0.2",
58
- "prettier": "^3.5.3",
59
- "prettier-plugin-svelte": "^3.3.3",
60
- "sass": "^1.86.3",
61
- "svelte": "^5.25.6",
62
- "svelte-check": "^4.1.5",
51
+ "@types/google.accounts": "^0.0.18",
52
+ "@types/jsonwebtoken": "^9.0.10",
53
+ "@types/pg": "^8.18.0",
54
+ "eslint": "^10.0.3",
55
+ "eslint-config-prettier": "^10.1.8",
56
+ "eslint-plugin-svelte": "^3.15.2",
57
+ "globals": "^17.4.0",
58
+ "jsonwebtoken": "^9.0.3",
59
+ "playwright": "^1.58.2",
60
+ "prettier": "^3.8.1",
61
+ "prettier-plugin-sql": "^0.19.2",
62
+ "prettier-plugin-svelte": "^3.5.1",
63
+ "prettier-plugin-tailwindcss": "^0.7.2",
64
+ "svelte": "^5.53.10",
65
+ "svelte-check": "^4.4.5",
63
66
  "tslib": "^2.8.1",
64
- "typescript": "^5.8.2",
65
- "typescript-eslint": "^8.29.0",
66
- "vite": "^6.2.5",
67
- "vitest": "^3.1.1"
67
+ "typescript": "^5.9.3",
68
+ "typescript-eslint": "^8.57.0",
69
+ "vite": "^7.3.1",
70
+ "vitest": "^4.0.18",
71
+ "vitest-browser-svelte": "^2.0.2"
68
72
  },
69
- "packageManager": "yarn@4.8.1",
70
- "prettier": "./prettier.config.mjs"
73
+ "packageManager": "yarn@4.13.0"
71
74
  }
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from '@playwright/test'
2
+
3
+ export default defineConfig({
4
+ testDir: './tests',
5
+ testMatch: '**/*.e2e.test.ts',
6
+ use: {
7
+ baseURL: 'http://localhost:3000'
8
+ },
9
+ webServer: {
10
+ // In local dev, use the same dev server you run manually.
11
+ // In CI, fall back to a production-like preview build.
12
+ command: process.env.CI
13
+ ? 'yarn build && yarn preview -- --port 3000'
14
+ : 'yarn dev -- --port 3000',
15
+ env: {
16
+ ...process.env,
17
+ E2E_BYPASS_RECAPTCHA: 'true',
18
+ E2E_BRAINTREE_UNIQUE_ORDER_ID: 'true'
19
+ },
20
+ url: 'http://localhost:3000',
21
+ reuseExistingServer: !process.env.CI,
22
+ timeout: 120_000
23
+ }
24
+ })
@@ -1,17 +1,26 @@
1
1
  export default {
2
2
  $schema: 'https://json.schemastore.org/prettierrc',
3
3
  useTabs: true,
4
- tabWidth: 2,
5
- semi: false,
6
- arrowParens: 'avoid',
7
4
  singleQuote: true,
8
5
  trailingComma: 'none',
9
6
  printWidth: 100,
10
- plugins: ['prettier-plugin-svelte'],
7
+ tabWidth: 2,
8
+ semi: false,
9
+ arrowParens: 'avoid',
10
+
11
+ plugins: ['prettier-plugin-svelte', 'prettier-plugin-sql', 'prettier-plugin-tailwindcss'],
11
12
  overrides: [
12
13
  {
13
14
  files: '*.svelte',
14
15
  options: { parser: 'svelte' }
16
+ },
17
+ {
18
+ files: '*.sql',
19
+ options: {
20
+ parser: 'sql',
21
+ language: 'postgresql'
22
+ }
15
23
  }
16
- ]
24
+ ],
25
+ tailwindStylesheet: './src/routes/layout.css'
17
26
  }
package/src/app.html CHANGED
@@ -12,7 +12,7 @@
12
12
  ></script>
13
13
  %sveltekit.head%
14
14
  </head>
15
- <body>
15
+ <body data-sveltekit-preload-data="hover">
16
16
  <div id="svelte">%sveltekit.body%</div>
17
17
  </body>
18
18
  </html>
@@ -3,8 +3,7 @@ import { query } from '$lib/server/db'
3
3
 
4
4
  // Attach authorization to each server request (role may have changed)
5
5
  async function attachUserToRequestEvent(sessionId: string, event: RequestEvent) {
6
- const sql = `SELECT * FROM get_session($1);`
7
- const { rows } = await query(sql, [sessionId])
6
+ const { rows } = await query('SELECT * FROM get_session($1::uuid)', [sessionId], 'get-session')
8
7
  if (rows?.length > 0) {
9
8
  event.locals.user = <User>rows[0].get_session
10
9
  }
@@ -12,10 +11,15 @@ async function attachUserToRequestEvent(sessionId: string, event: RequestEvent)
12
11
 
13
12
  // Invoked for each endpoint called and initially for SSR router
14
13
  export const handle: Handle = async ({ event, resolve }) => {
15
- const { cookies } = event
16
- const sessionId = cookies.get('session')
14
+ const { cookies, url } = event
15
+
16
+ // Skip auth overhead for static asset requests
17
+ if (url.pathname.startsWith('/_app/')) {
18
+ return resolve(event)
19
+ }
17
20
 
18
21
  // before endpoint or page is called
22
+ const sessionId = cookies.get('session')
19
23
  if (sessionId) {
20
24
  await attachUserToRequestEvent(sessionId, event)
21
25
  }
@@ -1,16 +1,68 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { QueryResult } from 'pg'
1
+ import type { QueryResult, QueryResultRow } from 'pg'
3
2
  import pg from 'pg'
4
- import { DATABASE_URL } from '$env/static/private'
3
+ import { env } from '$env/dynamic/private'
4
+
5
+ /**
6
+ * Generic function signature for executing a typed SQL query.
7
+ *
8
+ * @template T The row type returned from the database, extending QueryResultRow.
9
+ * @param sql The parameterized SQL query string.
10
+ * @param params Optional positional parameters to bind to the query.
11
+ * @returns A promise resolving to the typed QueryResult.
12
+ */
13
+ type QueryFunction = <T extends QueryResultRow>(
14
+ sql: string,
15
+ params?: (string | number | boolean | object | null)[],
16
+ name?: string
17
+ ) => Promise<QueryResult<T>>
18
+
19
+ let queryFn: QueryFunction
5
20
 
6
21
  const pool = new pg.Pool({
7
22
  max: 10, // default
8
- connectionString: DATABASE_URL,
9
- ssl: {
10
- // If your postgresql.conf does not have `ssl = on`, remove the entire ssl property or you will get an error
11
- rejectUnauthorized: false
12
- }
23
+ idleTimeoutMillis: 10000,
24
+ connectionTimeoutMillis: 5000,
25
+ connectionString: env.DATABASE_URL,
26
+ ssl: env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false
13
27
  })
14
28
 
15
- type PostgresQueryResult = (sql: string, params?: any[]) => Promise<QueryResult<any>>
16
- export const query: PostgresQueryResult = (sql, params?) => pool.query(sql, params)
29
+ pool.on('error', (err: Error) => {
30
+ console.error('Unexpected error on idle client', err)
31
+ })
32
+
33
+ queryFn = <T extends QueryResultRow>(
34
+ sql: string,
35
+ params?: (string | number | boolean | object | null)[],
36
+ name?: string
37
+ ) => pool.query<T>(name ? { name, text: sql, values: params } : { text: sql, values: params })
38
+
39
+ /**
40
+ * Executes a parameterized SQL query against the PostgreSQL database.
41
+ *
42
+ * @template T - The expected shape of rows returned by the query. Must extend QueryResultRow.
43
+ * @param sql - The SQL query string. Use $1, $2, etc. for parameterized queries.
44
+ * @param params - Optional array of parameter values to bind to the query placeholders.
45
+ * @returns A Promise resolving to a PostgreSQL QueryResult containing typed rows and metadata.
46
+ * @throws {Error} If the database connection fails or the query is invalid.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * // Simple query without parameters
51
+ * const result = await query('SELECT * FROM users');
52
+ * console.log(result.rows);
53
+ *
54
+ * // Parameterized query with type safety
55
+ * const result = await query<User>('SELECT * FROM users WHERE id = $1', [userId]);
56
+ * console.log(result.rows[0].name); // TypeScript knows this is a User
57
+ *
58
+ * // Insert with multiple parameters
59
+ * await query(
60
+ * 'INSERT INTO products (name, price) VALUES ($1, $2)',
61
+ * ['Widget', 29.99]
62
+ * );
63
+ * ```
64
+ */
65
+ export const query = <T extends QueryResultRow = any>(
66
+ sql: string,
67
+ params?: (string | number | boolean | object | null)[]
68
+ ): Promise<QueryResult<T>> => queryFn<T>(sql, params)
@@ -1,6 +1,6 @@
1
1
  <script lang="ts" module>
2
- import { page } from '$app/stores'
2
+ import { page } from '$app/state'
3
3
  </script>
4
4
 
5
- <h1>{$page.status}</h1>
6
- <h4>{$page.error?.message}</h4>
5
+ <h1>{page.status}</h1>
6
+ <h4>{page.error?.message}</h4>