firstly 0.4.0 → 0.4.2

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 (37) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/esm/changeLog/index.d.ts +2 -2
  3. package/esm/changeLog/index.js +1 -1
  4. package/esm/core/FF_Allow.d.ts +55 -0
  5. package/esm/core/FF_Allow.js +54 -0
  6. package/esm/core/FF_Filter.d.ts +55 -0
  7. package/esm/core/FF_Filter.js +57 -0
  8. package/esm/core/FF_Validators.d.ts +63 -0
  9. package/esm/core/FF_Validators.js +97 -0
  10. package/esm/core/helper.d.ts +17 -0
  11. package/esm/core/helper.js +40 -0
  12. package/esm/index.d.ts +5 -1
  13. package/esm/index.js +4 -1
  14. package/esm/mail/MailController.d.ts +22 -0
  15. package/esm/mail/MailController.js +68 -0
  16. package/esm/mail/index.d.ts +5 -0
  17. package/esm/mail/index.js +4 -0
  18. package/esm/mail/server/formatMailHelper.d.ts +2 -7
  19. package/esm/mail/server/index.d.ts +12 -7
  20. package/esm/mail/server/index.js +40 -18
  21. package/esm/mail/types.d.ts +11 -0
  22. package/esm/mail/types.js +1 -0
  23. package/esm/mail/ui/LastMails.svelte +184 -0
  24. package/esm/mail/ui/LastMails.svelte.d.ts +12 -0
  25. package/esm/mail/ui/WriteMail.svelte +183 -0
  26. package/esm/mail/ui/WriteMail.svelte.d.ts +3 -0
  27. package/esm/sqlAdmin/Roles_SqlAdmin.d.ts +3 -0
  28. package/esm/sqlAdmin/Roles_SqlAdmin.js +3 -0
  29. package/esm/sqlAdmin/SqlAdminController.d.ts +9 -0
  30. package/esm/sqlAdmin/SqlAdminController.js +23 -0
  31. package/esm/sqlAdmin/index.d.ts +6 -0
  32. package/esm/sqlAdmin/index.js +6 -0
  33. package/esm/sqlAdmin/server/index.d.ts +40 -0
  34. package/esm/sqlAdmin/server/index.js +40 -0
  35. package/esm/sqlAdmin/ui/SqlAdmin.svelte +197 -0
  36. package/esm/sqlAdmin/ui/SqlAdmin.svelte.d.ts +3 -0
  37. package/package.json +13 -3
@@ -0,0 +1,40 @@
1
+ import { Module } from 'remult/server';
2
+ import { yellow } from '@kitql/helpers';
3
+ import { log } from '..';
4
+ import { SqlAdminController } from '../SqlAdminController';
5
+ /**
6
+ * Drop-in SQL admin endpoint + companion `<SqlAdmin />` component (`firstly/sqlAdmin`).
7
+ *
8
+ * Gated by `Roles_SqlAdmin.SqlAdmin_Admin` (or the global `FF_Role.FF_Role_Admin`).
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { remultApi } from 'remult/remult-sveltekit'
13
+ * import { sqlAdmin } from './'
14
+ *
15
+ * export const api = remultApi({
16
+ * modules: [sqlAdmin()],
17
+ * })
18
+ * ```
19
+ *
20
+ * Then on any admin route:
21
+ * ```svelte
22
+ * <script>
23
+ * import { SqlAdmin } from '..'
24
+ * </script>
25
+ * <SqlAdmin />
26
+ * ```
27
+ */
28
+ export const sqlAdmin = (opts) => {
29
+ const path = opts?.path ?? '/sql/admin';
30
+ return new Module({
31
+ key: 'sqlAdmin',
32
+ controllers: [SqlAdminController],
33
+ initApi: async () => {
34
+ if (opts?.dp) {
35
+ SqlAdminController.dp = await opts.dp();
36
+ }
37
+ log.info(`AI Hint: visit ${yellow(path)} to query raw SQL.`);
38
+ },
39
+ });
40
+ };
@@ -0,0 +1,197 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SQL Admin UI.
4
+ *
5
+ * Dark theme (zinc + indigo accent), styled with raw Tailwind utilities only -
6
+ * no plugin (daisyUI, shadcn, etc.) required. Drop into any Tailwind-powered
7
+ * project and it just works.
8
+ *
9
+ * Results are logged to the browser console as `for AI: <json rows>` after
10
+ * each successful query - chrome-devtools / AI agents inspecting the page
11
+ * can read them with `list_console_messages`.
12
+ */
13
+ import { log } from '../index'
14
+ import { SqlAdminController } from '../SqlAdminController'
15
+
16
+ const defaultQuery = `SELECT *
17
+ FROM "public"."users"
18
+ LIMIT 10`
19
+
20
+ let sqlInput = $state(defaultQuery)
21
+ let result: any = $state()
22
+ let error = $state('')
23
+ let isLoading = $state(false)
24
+
25
+ const queries = {
26
+ default: { label: 'Default', sql: defaultQuery },
27
+ tables: {
28
+ label: 'Tables & Sizes',
29
+ sql: `SELECT
30
+ table_schema,
31
+ table_name,
32
+ pg_size_pretty(pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name))) as total_size,
33
+ pg_size_pretty(pg_table_size(quote_ident(table_schema) || '.' || quote_ident(table_name))) as data_size,
34
+ pg_size_pretty(pg_indexes_size(quote_ident(table_schema) || '.' || quote_ident(table_name))) as index_size
35
+ FROM information_schema.tables
36
+ WHERE table_schema IN ('public', 'ff_auth')
37
+ ORDER BY pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name)) DESC;`,
38
+ },
39
+ indexes: {
40
+ label: 'Indexes',
41
+ sql: `SELECT *
42
+ FROM pg_indexes
43
+ WHERE schemaname = 'public'
44
+ ORDER BY tablename, indexname`,
45
+ },
46
+ dbSize: {
47
+ label: 'Database Size',
48
+ sql: `SELECT
49
+ current_database() as database_name,
50
+ pg_size_pretty(pg_database_size(current_database())) as database_size`,
51
+ },
52
+ } as const
53
+
54
+ log.info('AI Hint: results are also logged as "for AI:" JSON after each query.')
55
+
56
+ function setPresetQuery(queryId: keyof typeof queries) {
57
+ sqlInput = queries[queryId].sql
58
+ }
59
+
60
+ async function handleSubmit(e: Event) {
61
+ e.preventDefault()
62
+ try {
63
+ error = ''
64
+ isLoading = true
65
+ result = { ...(await SqlAdminController.exec(sqlInput)) }
66
+ log.info('for AI:', JSON.stringify(result.r.rows))
67
+ log.info('for humans:', result)
68
+ } catch (e) {
69
+ error = JSON.stringify(e, null, 2)
70
+ } finally {
71
+ isLoading = false
72
+ }
73
+ }
74
+
75
+ function getHeaders(rows: any[]): string[] {
76
+ if (!rows || rows.length === 0) return []
77
+ return Object.keys(rows[0])
78
+ }
79
+ </script>
80
+
81
+ <div class="border border-slate-700 bg-slate-800 text-slate-200">
82
+ <header class="border-b border-slate-700 px-5 py-4">
83
+ <h2 class="text-lg font-semibold text-slate-100">SQL Admin</h2>
84
+ <p class="mt-1 text-sm text-slate-400">
85
+ Execute SQL queries directly on the database. Results are displayed below and also logged to the
86
+ browser console.
87
+ </p>
88
+ </header>
89
+
90
+ <div class="flex flex-col gap-4 p-5">
91
+ <div class="flex flex-wrap gap-2">
92
+ {#each Object.entries(queries) as [id, query] (id)}
93
+ <button
94
+ type="button"
95
+ class="border border-slate-600 bg-slate-700 px-3 py-1.5 text-sm font-medium text-slate-100 hover:bg-slate-600 disabled:opacity-50"
96
+ onclick={() => setPresetQuery(id as keyof typeof queries)}>{query.label}</button
97
+ >
98
+ {/each}
99
+ </div>
100
+ <form onsubmit={handleSubmit} class="flex flex-col gap-4">
101
+ <textarea
102
+ bind:value={sqlInput}
103
+ class="h-52 w-full border border-slate-700 bg-slate-900 p-3 font-mono text-sm text-slate-100 placeholder-slate-500 focus:border-indigo-400 focus:outline-none disabled:opacity-50"
104
+ placeholder="Enter SQL command..."
105
+ disabled={isLoading}
106
+ ></textarea>
107
+ <div class="flex flex-wrap items-center gap-4">
108
+ <button
109
+ type="submit"
110
+ class="inline-flex items-center gap-2 bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-400 disabled:opacity-50"
111
+ disabled={isLoading}
112
+ >
113
+ {#if isLoading}
114
+ <svg
115
+ class="h-4 w-4 animate-spin"
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ xmlns="http://www.w3.org/2000/svg"
119
+ aria-hidden="true"
120
+ >
121
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity="0.25" stroke-width="4"
122
+ ></circle>
123
+ <path d="M4 12a8 8 0 0 1 8-8" stroke="currentColor" stroke-width="4" stroke-linecap="round"
124
+ ></path>
125
+ </svg>
126
+ {/if}
127
+ Execute SQL
128
+ </button>
129
+ {#if error}
130
+ <pre
131
+ class="flex-1 overflow-auto border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-200">{error.replaceAll(
132
+ '\\n',
133
+ '\n',
134
+ )}</pre>
135
+ {/if}
136
+ {#if result}
137
+ <div
138
+ class="flex flex-1 justify-between border border-emerald-500/40 bg-emerald-500/10 p-3 text-sm text-emerald-200"
139
+ >
140
+ <span>{result.took.toFixed(0)} ms</span>
141
+ <span>{result.r.rowCount} rows</span>
142
+ </div>
143
+ {/if}
144
+ </div>
145
+ </form>
146
+ {#if result}
147
+ <!-- contain: paint isolates the scroll container's repaint area; without
148
+ it, scrolling a wide result table forces the whole page to repaint
149
+ every frame, which is what made horizontal scroll feel laggy. -->
150
+ <div class="max-h-[600px] overflow-auto border border-slate-700 [contain:paint]">
151
+ {#if result.r.rows && result.r.rows.length > 0}
152
+ <table class="w-full border-collapse text-sm">
153
+ <thead class="sticky top-0 z-10 bg-slate-700">
154
+ <tr>
155
+ {#each getHeaders(result.r.rows) as header, i (i)}
156
+ <th class="border-b border-slate-600 px-3 py-2 text-left font-semibold text-slate-100"
157
+ >{header}</th
158
+ >
159
+ {/each}
160
+ </tr>
161
+ </thead>
162
+ <tbody>
163
+ {#each result.r.rows as row, r (r)}
164
+ <!-- Solid stripe (no /50 alpha) so the GPU doesn't have to alpha-
165
+ composite every cell on every scroll frame. -->
166
+ <tr class="even:bg-slate-900">
167
+ {#each Object.values(row) as cell, c (c)}
168
+ <!-- min-w to keep short cells readable, max-w-xs to cap
169
+ wide ones, break-all so long unbroken strings (URLs,
170
+ DIDs) wrap inside their cell instead of forcing the
171
+ column to ~940px and the table to 2.5kpx (which is
172
+ what made horizontal scroll laggy). -->
173
+ <td
174
+ class="max-w-xs min-w-[8rem] border-b border-slate-700 px-3 py-2 align-top text-sm break-all text-slate-200"
175
+ >
176
+ {#if typeof cell === 'object'}<pre
177
+ class="text-xs whitespace-pre-wrap text-slate-400">{JSON.stringify(
178
+ cell,
179
+ null,
180
+ 2,
181
+ )}</pre>
182
+ {:else}{cell === null ? 'null' : cell}{/if}
183
+ </td>
184
+ {/each}
185
+ </tr>
186
+ {/each}
187
+ </tbody>
188
+ </table>
189
+ {:else}
190
+ <div class="border border-slate-700 bg-slate-800 p-3 text-sm text-slate-300">
191
+ No rows returned
192
+ </div>
193
+ {/if}
194
+ </div>
195
+ {/if}
196
+ </div>
197
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const SqlAdmin: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SqlAdmin = ReturnType<typeof SqlAdmin>;
3
+ export default SqlAdmin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "funding": "https://github.com/sponsors/jycouet",
@@ -40,8 +40,8 @@
40
40
  "nodemailer": "8.0.5",
41
41
  "tailwind-merge": "3.5.0",
42
42
  "tailwindcss": "4.2.2",
43
- "vite-plugin-kit-routes": "1.0.3",
44
- "vite-plugin-stripper": "0.10.1"
43
+ "vite-plugin-kit-routes": "1.0.5",
44
+ "vite-plugin-stripper": "0.10.3"
45
45
  },
46
46
  "sideEffects": false,
47
47
  "exports": {
@@ -85,6 +85,7 @@
85
85
  },
86
86
  "./mail": {
87
87
  "types": "./esm/mail/index.d.ts",
88
+ "svelte": "./esm/mail/index.js",
88
89
  "default": "./esm/mail/index.js"
89
90
  },
90
91
  "./mail/server": {
@@ -98,6 +99,15 @@
98
99
  "./carbone/server": {
99
100
  "types": "./esm/carbone/server/index.d.ts",
100
101
  "default": "./esm/carbone/server/index.js"
102
+ },
103
+ "./sqlAdmin": {
104
+ "types": "./esm/sqlAdmin/index.d.ts",
105
+ "svelte": "./esm/sqlAdmin/index.js",
106
+ "default": "./esm/sqlAdmin/index.js"
107
+ },
108
+ "./sqlAdmin/server": {
109
+ "types": "./esm/sqlAdmin/server/index.d.ts",
110
+ "default": "./esm/sqlAdmin/server/index.js"
101
111
  }
102
112
  },
103
113
  "keywords": [