firstly 0.4.0 → 0.4.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # firstly
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#250](https://github.com/jycouet/firstly/pull/250) [`53916ad`](https://github.com/jycouet/firstly/commit/53916ad519835a47e3598afb68ce6d84356af944) Thanks [@jycouet](https://github.com/jycouet)! - Add `firstly/sqlAdmin` module: a drop-in `<SqlAdmin />` Svelte component plus a `BackendMethod` controller gated by `Roles_SqlAdmin.SqlAdmin_Admin` (or `FF_Role.FF_Role_Admin`). The component ships with prefilled queries (DB size, table sizes, indexes) and logs results as `for AI: <rows>` to the browser console for chrome-devtools / AI-agent inspection. `sqlAdmin({ path })` logs an AI hint on server start pointing to the page (default `/sql/admin`).
8
+
9
+ Add `FF_Allow` and `FF_Filter` helpers (exported from `firstly`) for owner-only / admin-or-owner row checks and prefilters - usable in `allowApi*` and `apiPrefilter`. Both accept an entity generic (`FF_Allow.owner<Task>('userId')`) for type-safe column names; `col` defaults to `'userId'` if omitted. The `ownerOr<T>({ col?, roles })` variants are shortcuts for the "admin (or any role) OR owner" pattern.
10
+
3
11
  ## 0.4.0
4
12
 
5
13
  ### Minor Changes
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Row-level allow helpers (for `allowApiUpdate`, `allowApiDelete`, ...).
3
+ *
4
+ * Pair with `FF_Filter` (the equivalent for `apiPrefilter`).
5
+ *
6
+ * Pass the entity type as a generic (`FF_Allow.owner<Task>('userId')`) to get
7
+ * autocompletion and type-safety on the column name. Without a generic the
8
+ * column is just a `string`.
9
+ */
10
+ export declare const FF_Allow: {
11
+ /**
12
+ * Allow only when the row's `col` equals the current user id.
13
+ *
14
+ * `col` defaults to `'userId'` if omitted.
15
+ *
16
+ * @example
17
+ * Owner-only update / delete (typed):
18
+ * ```ts
19
+ * import { FF_Entity, FF_Allow } from '..'
20
+ *
21
+ * \@FF_Entity<Task>('tasks', {
22
+ * allowApiUpdate: FF_Allow.owner<Task>('userId'), // typed: 'userId' must be a key of Task
23
+ * allowApiDelete: FF_Allow.owner<Task>(), // defaults to 'userId'
24
+ * })
25
+ * export class Task { ... }
26
+ * ```
27
+ *
28
+ * For "admin OR owner", prefer `FF_Allow.ownerOr` instead of inlining the
29
+ * combination yourself.
30
+ */
31
+ owner: <T>(col?: keyof T & string) => (entity?: T) => boolean;
32
+ /**
33
+ * Allow when the current user has any of the given `roles`, OR when the
34
+ * row's `col` equals the current user id.
35
+ *
36
+ * `col` defaults to `'userId'` if omitted.
37
+ *
38
+ * @example
39
+ * Admin OR owner (typed):
40
+ * ```ts
41
+ * import { FF_Entity, FF_Allow } from '..'
42
+ * import { Roles } from '../roles'
43
+ *
44
+ * \@FF_Entity<Task>('tasks', {
45
+ * allowApiUpdate: FF_Allow.ownerOr<Task>({ roles: [Roles.Admin] }),
46
+ * allowApiDelete: FF_Allow.ownerOr<Task>({ col: 'createdBy', roles: [Roles.Admin] }),
47
+ * })
48
+ * export class Task { ... }
49
+ * ```
50
+ */
51
+ ownerOr: <T>({ col, roles }: {
52
+ col?: keyof T & string;
53
+ roles: string[];
54
+ }) => (entity?: T) => boolean;
55
+ };
@@ -0,0 +1,54 @@
1
+ import { remult } from 'remult';
2
+ /**
3
+ * Row-level allow helpers (for `allowApiUpdate`, `allowApiDelete`, ...).
4
+ *
5
+ * Pair with `FF_Filter` (the equivalent for `apiPrefilter`).
6
+ *
7
+ * Pass the entity type as a generic (`FF_Allow.owner<Task>('userId')`) to get
8
+ * autocompletion and type-safety on the column name. Without a generic the
9
+ * column is just a `string`.
10
+ */
11
+ export const FF_Allow = {
12
+ /**
13
+ * Allow only when the row's `col` equals the current user id.
14
+ *
15
+ * `col` defaults to `'userId'` if omitted.
16
+ *
17
+ * @example
18
+ * Owner-only update / delete (typed):
19
+ * ```ts
20
+ * import { FF_Entity, FF_Allow } from '..'
21
+ *
22
+ * \@FF_Entity<Task>('tasks', {
23
+ * allowApiUpdate: FF_Allow.owner<Task>('userId'), // typed: 'userId' must be a key of Task
24
+ * allowApiDelete: FF_Allow.owner<Task>(), // defaults to 'userId'
25
+ * })
26
+ * export class Task { ... }
27
+ * ```
28
+ *
29
+ * For "admin OR owner", prefer `FF_Allow.ownerOr` instead of inlining the
30
+ * combination yourself.
31
+ */
32
+ owner: (col = 'userId') => (entity) => !!remult.user?.id && entity?.[col] === remult.user.id,
33
+ /**
34
+ * Allow when the current user has any of the given `roles`, OR when the
35
+ * row's `col` equals the current user id.
36
+ *
37
+ * `col` defaults to `'userId'` if omitted.
38
+ *
39
+ * @example
40
+ * Admin OR owner (typed):
41
+ * ```ts
42
+ * import { FF_Entity, FF_Allow } from '..'
43
+ * import { Roles } from '../roles'
44
+ *
45
+ * \@FF_Entity<Task>('tasks', {
46
+ * allowApiUpdate: FF_Allow.ownerOr<Task>({ roles: [Roles.Admin] }),
47
+ * allowApiDelete: FF_Allow.ownerOr<Task>({ col: 'createdBy', roles: [Roles.Admin] }),
48
+ * })
49
+ * export class Task { ... }
50
+ * ```
51
+ */
52
+ ownerOr: ({ col = 'userId', roles }) => (entity) => roles.some((r) => remult.isAllowed(r)) ||
53
+ (!!remult.user?.id && entity?.[col] === remult.user.id),
54
+ };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Prefilter helpers (for `apiPrefilter`, `backendPrefilter`).
3
+ *
4
+ * Pair with `FF_Allow` (the equivalent for `allowApi*` row checks).
5
+ *
6
+ * Pass the entity type as a generic (`FF_Filter.owner<Task>('userId')`) to get
7
+ * autocompletion and type-safety on the column name. Without a generic the
8
+ * column is just a `string`.
9
+ */
10
+ export declare const FF_Filter: {
11
+ /**
12
+ * Prefilter rows where `col` equals the current user id.
13
+ *
14
+ * `col` defaults to `'userId'` if omitted. When anonymous, yields
15
+ * `IN (NULL)` which matches nothing.
16
+ *
17
+ * @example
18
+ * Owner-only prefilter (typed):
19
+ * ```ts
20
+ * import { FF_Entity, FF_Filter } from '..'
21
+ *
22
+ * \@FF_Entity<Task>('tasks', {
23
+ * apiPrefilter: () => FF_Filter.owner<Task>('userId'),
24
+ * })
25
+ * export class Task { ... }
26
+ * ```
27
+ *
28
+ * For "admin sees all, others see only their own", prefer
29
+ * `FF_Filter.ownerOr` instead of inlining the combination yourself.
30
+ */
31
+ owner: <T>(col?: keyof T & string) => any;
32
+ /**
33
+ * Prefilter that returns `{}` (no filter) when the current user has any of
34
+ * the given `roles`, otherwise restricts to rows where `col` equals the
35
+ * current user id.
36
+ *
37
+ * `col` defaults to `'userId'` if omitted.
38
+ *
39
+ * @example
40
+ * Admin sees all, others only their own (typed):
41
+ * ```ts
42
+ * import { FF_Entity, FF_Filter } from '..'
43
+ * import { Roles } from '../roles'
44
+ *
45
+ * \@FF_Entity<Task>('tasks', {
46
+ * apiPrefilter: () => FF_Filter.ownerOr<Task>({ roles: [Roles.Admin] }),
47
+ * })
48
+ * export class Task { ... }
49
+ * ```
50
+ */
51
+ ownerOr: <T>({ col, roles, }: {
52
+ col?: keyof T & string;
53
+ roles: string[];
54
+ }) => any;
55
+ };
@@ -0,0 +1,57 @@
1
+ import { remult } from 'remult';
2
+ /**
3
+ * Prefilter helpers (for `apiPrefilter`, `backendPrefilter`).
4
+ *
5
+ * Pair with `FF_Allow` (the equivalent for `allowApi*` row checks).
6
+ *
7
+ * Pass the entity type as a generic (`FF_Filter.owner<Task>('userId')`) to get
8
+ * autocompletion and type-safety on the column name. Without a generic the
9
+ * column is just a `string`.
10
+ */
11
+ export const FF_Filter = {
12
+ /**
13
+ * Prefilter rows where `col` equals the current user id.
14
+ *
15
+ * `col` defaults to `'userId'` if omitted. When anonymous, yields
16
+ * `IN (NULL)` which matches nothing.
17
+ *
18
+ * @example
19
+ * Owner-only prefilter (typed):
20
+ * ```ts
21
+ * import { FF_Entity, FF_Filter } from '..'
22
+ *
23
+ * \@FF_Entity<Task>('tasks', {
24
+ * apiPrefilter: () => FF_Filter.owner<Task>('userId'),
25
+ * })
26
+ * export class Task { ... }
27
+ * ```
28
+ *
29
+ * For "admin sees all, others see only their own", prefer
30
+ * `FF_Filter.ownerOr` instead of inlining the combination yourself.
31
+ */
32
+ owner: (col = 'userId') => ({ [col]: [remult.user?.id] }),
33
+ /**
34
+ * Prefilter that returns `{}` (no filter) when the current user has any of
35
+ * the given `roles`, otherwise restricts to rows where `col` equals the
36
+ * current user id.
37
+ *
38
+ * `col` defaults to `'userId'` if omitted.
39
+ *
40
+ * @example
41
+ * Admin sees all, others only their own (typed):
42
+ * ```ts
43
+ * import { FF_Entity, FF_Filter } from '..'
44
+ * import { Roles } from '../roles'
45
+ *
46
+ * \@FF_Entity<Task>('tasks', {
47
+ * apiPrefilter: () => FF_Filter.ownerOr<Task>({ roles: [Roles.Admin] }),
48
+ * })
49
+ * export class Task { ... }
50
+ * ```
51
+ */
52
+ ownerOr: ({ col = 'userId', roles, }) => {
53
+ if (roles.some((r) => remult.isAllowed(r)))
54
+ return {};
55
+ return { [col]: [remult.user?.id] };
56
+ },
57
+ };
package/esm/index.d.ts CHANGED
@@ -7,6 +7,8 @@ export { BaseEnum } from './core/BaseEnum.js';
7
7
  export type { BaseEnumOptions, BaseItem, BaseItemLight, FF_Icon } from './core/BaseEnum.js';
8
8
  export { FF_Entity } from './core/FF_Entity.js';
9
9
  export { FF_Role } from './core/common.js';
10
+ export { FF_Allow } from './core/FF_Allow.js';
11
+ export { FF_Filter } from './core/FF_Filter.js';
10
12
  export { isError } from './core/helper.js';
11
13
  export { tryCatch, tryCatchSync } from './core/tryCatch.js';
12
14
  export type { ResolvedType, UnArray, RecursivePartial } from './core/types.js';
package/esm/index.js CHANGED
@@ -9,6 +9,8 @@ export const ff_Log = new h.Log('firstly');
9
9
  export { BaseEnum } from './core/BaseEnum.js';
10
10
  export { FF_Entity } from './core/FF_Entity.js';
11
11
  export { FF_Role } from './core/common.js';
12
+ export { FF_Allow } from './core/FF_Allow.js';
13
+ export { FF_Filter } from './core/FF_Filter.js';
12
14
  export { isError } from './core/helper.js';
13
15
  export { tryCatch, tryCatchSync } from './core/tryCatch.js';
14
16
  export { tw } from './core/tailwind.js';
@@ -0,0 +1,3 @@
1
+ export declare const Roles_SqlAdmin: {
2
+ readonly SqlAdmin_Admin: "SqlAdmin.Admin";
3
+ };
@@ -0,0 +1,3 @@
1
+ export const Roles_SqlAdmin = {
2
+ SqlAdmin_Admin: 'SqlAdmin.Admin',
3
+ };
@@ -0,0 +1,9 @@
1
+ import { SqlDatabase } from 'remult';
2
+ export declare class SqlAdminController {
3
+ /** Optional override set by the `sqlAdmin()` module's `initApi`. Falls back to `SqlDatabase.getDb()`. */
4
+ static dp?: SqlDatabase;
5
+ static exec(cmd: string): Promise<{
6
+ r: import("remult").SqlResult;
7
+ took: number;
8
+ }>;
9
+ }
@@ -0,0 +1,23 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BackendMethod, SqlDatabase } from 'remult';
8
+ import { FF_Role } from '../core/common';
9
+ import { Roles_SqlAdmin } from './Roles_SqlAdmin';
10
+ export class SqlAdminController {
11
+ /** Optional override set by the `sqlAdmin()` module's `initApi`. Falls back to `SqlDatabase.getDb()`. */
12
+ static dp;
13
+ static async exec(cmd) {
14
+ const db = SqlAdminController.dp ?? SqlDatabase.getDb();
15
+ const start = performance.now();
16
+ const r = await db.execute(cmd);
17
+ const took = performance.now() - start;
18
+ return { r, took };
19
+ }
20
+ }
21
+ __decorate([
22
+ BackendMethod({ allowed: [Roles_SqlAdmin.SqlAdmin_Admin, FF_Role.FF_Role_Admin] })
23
+ ], SqlAdminController, "exec", null);
@@ -0,0 +1,6 @@
1
+ import { Log } from '@kitql/helpers';
2
+ export { Roles_SqlAdmin } from './Roles_SqlAdmin';
3
+ export { SqlAdminController } from './SqlAdminController';
4
+ export { default as SqlAdmin } from './ui/SqlAdmin.svelte';
5
+ export declare const key = "sqlAdmin";
6
+ export declare const log: Log;
@@ -0,0 +1,6 @@
1
+ import { Log } from '@kitql/helpers';
2
+ export { Roles_SqlAdmin } from './Roles_SqlAdmin';
3
+ export { SqlAdminController } from './SqlAdminController';
4
+ export { default as SqlAdmin } from './ui/SqlAdmin.svelte';
5
+ export const key = 'sqlAdmin';
6
+ export const log = new Log(key);
@@ -0,0 +1,40 @@
1
+ import type { SqlDatabase } from 'remult';
2
+ import { Module } from 'remult/server';
3
+ export type SqlAdminOptions = {
4
+ /**
5
+ * Override the SqlDatabase used to execute queries.
6
+ * Defaults to `SqlDatabase.getDb()` (the active Remult data provider).
7
+ */
8
+ dp?: () => SqlDatabase | Promise<SqlDatabase>;
9
+ /**
10
+ * The route where you mounted the `<SqlAdmin />` component.
11
+ * Used only for the AI hint logged on server start.
12
+ *
13
+ * @default '/sql/admin'
14
+ */
15
+ path?: string;
16
+ };
17
+ /**
18
+ * Drop-in SQL admin endpoint + companion `<SqlAdmin />` component (`firstly/sqlAdmin`).
19
+ *
20
+ * Gated by `Roles_SqlAdmin.SqlAdmin_Admin` (or the global `FF_Role.FF_Role_Admin`).
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { remultApi } from 'remult/remult-sveltekit'
25
+ * import { sqlAdmin } from './'
26
+ *
27
+ * export const api = remultApi({
28
+ * modules: [sqlAdmin()],
29
+ * })
30
+ * ```
31
+ *
32
+ * Then on any admin route:
33
+ * ```svelte
34
+ * <script>
35
+ * import { SqlAdmin } from '..'
36
+ * </script>
37
+ * <SqlAdmin />
38
+ * ```
39
+ */
40
+ export declare const sqlAdmin: (opts?: SqlAdminOptions) => Module<unknown>;
@@ -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,146 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SQL Admin UI.
4
+ *
5
+ * Results are logged to the browser console as `for AI: <json rows>` after
6
+ * each successful query - chrome-devtools / AI agents inspecting the page
7
+ * can read them with `list_console_messages`.
8
+ */
9
+ import { SqlAdminController } from '../SqlAdminController'
10
+
11
+ const defaultQuery = `SELECT *
12
+ FROM "public"."users"
13
+ LIMIT 10`
14
+
15
+ let sqlInput = $state(defaultQuery)
16
+ let result: any = $state()
17
+ let error = $state('')
18
+ let isLoading = $state(false)
19
+
20
+ const queries = {
21
+ default: { label: 'Default', sql: defaultQuery },
22
+ tables: {
23
+ label: 'Tables & Sizes',
24
+ sql: `SELECT
25
+ table_schema,
26
+ table_name,
27
+ pg_size_pretty(pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name))) as total_size,
28
+ pg_size_pretty(pg_table_size(quote_ident(table_schema) || '.' || quote_ident(table_name))) as data_size,
29
+ pg_size_pretty(pg_indexes_size(quote_ident(table_schema) || '.' || quote_ident(table_name))) as index_size
30
+ FROM information_schema.tables
31
+ WHERE table_schema IN ('public', 'ff_auth')
32
+ ORDER BY pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name)) DESC;`,
33
+ },
34
+ indexes: {
35
+ label: 'Indexes',
36
+ sql: `SELECT *
37
+ FROM pg_indexes
38
+ WHERE schemaname = 'public'
39
+ ORDER BY tablename, indexname`,
40
+ },
41
+ dbSize: {
42
+ label: 'Database Size',
43
+ sql: `SELECT
44
+ current_database() as database_name,
45
+ pg_size_pretty(pg_database_size(current_database())) as database_size`,
46
+ },
47
+ } as const
48
+
49
+ console.info(
50
+ '[firstly/SqlAdmin] AI Hint: results are also logged as "for AI:" JSON after each query.',
51
+ )
52
+
53
+ function setPresetQuery(queryId: keyof typeof queries) {
54
+ sqlInput = queries[queryId].sql
55
+ }
56
+
57
+ async function handleSubmit(e: Event) {
58
+ e.preventDefault()
59
+ try {
60
+ error = ''
61
+ isLoading = true
62
+ result = { ...(await SqlAdminController.exec(sqlInput)) }
63
+ console.info('for AI:', JSON.stringify(result.r.rows))
64
+ console.info('for humans:', result)
65
+ } catch (e) {
66
+ error = JSON.stringify(e, null, 2)
67
+ } finally {
68
+ isLoading = false
69
+ }
70
+ }
71
+
72
+ function getHeaders(rows: any[]): string[] {
73
+ if (!rows || rows.length === 0) return []
74
+ return Object.keys(rows[0])
75
+ }
76
+ </script>
77
+
78
+ <div class="flex flex-col gap-4">
79
+ <div class="flex flex-col gap-2">
80
+ <h2 class="text-2xl font-bold">SQL Admin</h2>
81
+ <p class="text-sm text-base-content/70">
82
+ Execute SQL queries directly on the database. Results are displayed below and also logged to the
83
+ browser console.
84
+ </p>
85
+ </div>
86
+ <div class="flex flex-wrap gap-2">
87
+ {#each Object.entries(queries) as [id, query] (id)}
88
+ <button class="btn btn-outline btn-sm" onclick={() => setPresetQuery(id as keyof typeof queries)}
89
+ >{query.label}</button
90
+ >
91
+ {/each}
92
+ </div>
93
+ <form onsubmit={handleSubmit} class="flex flex-col gap-4">
94
+ <fieldset class="fieldset">
95
+ <textarea
96
+ bind:value={sqlInput}
97
+ class="textarea h-52 w-full font-mono"
98
+ placeholder="Enter SQL command..."
99
+ disabled={isLoading}
100
+ ></textarea>
101
+ </fieldset>
102
+ <div class="flex items-center gap-4">
103
+ <button type="submit" class="btn btn-primary" disabled={isLoading}>
104
+ {#if isLoading}<span class="loading loading-spinner"></span>{/if}
105
+ Execute SQL
106
+ </button>
107
+ {#if error}<pre class="alert flex-1 text-sm alert-error">{error.replaceAll(
108
+ '\\n',
109
+ '\n',
110
+ )}</pre>{/if}
111
+ {#if result}
112
+ <div class="alert flex flex-1 justify-between alert-success">
113
+ <span class="text-success-content">{result.took.toFixed(0)} ms</span>
114
+ <span class="text-success-content">{result.r.rowCount} rows</span>
115
+ </div>
116
+ {/if}
117
+ </div>
118
+ </form>
119
+ {#if result}
120
+ <div class="max-h-[600px] overflow-auto rounded-lg border border-base-300">
121
+ {#if result.r.rows && result.r.rows.length > 0}
122
+ <table class="table w-full table-zebra bg-base-200">
123
+ <thead class="sticky top-0 z-10 bg-base-300">
124
+ <tr
125
+ >{#each getHeaders(result.r.rows) as header, i (i)}<th>{header}</th>{/each}</tr
126
+ >
127
+ </thead>
128
+ <tbody>
129
+ {#each result.r.rows as row, r (r)}
130
+ <tr>
131
+ {#each Object.values(row) as cell, c (c)}
132
+ <td class="align-top">
133
+ {#if typeof cell === 'object'}<pre class="text-xs">{JSON.stringify(cell, null, 2)}</pre>
134
+ {:else}{cell === null ? 'null' : cell}{/if}
135
+ </td>
136
+ {/each}
137
+ </tr>
138
+ {/each}
139
+ </tbody>
140
+ </table>
141
+ {:else}
142
+ <div class="alert alert-info">No rows returned</div>
143
+ {/if}
144
+ </div>
145
+ {/if}
146
+ </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.1",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "funding": "https://github.com/sponsors/jycouet",
@@ -98,6 +98,15 @@
98
98
  "./carbone/server": {
99
99
  "types": "./esm/carbone/server/index.d.ts",
100
100
  "default": "./esm/carbone/server/index.js"
101
+ },
102
+ "./sqlAdmin": {
103
+ "types": "./esm/sqlAdmin/index.d.ts",
104
+ "svelte": "./esm/sqlAdmin/index.js",
105
+ "default": "./esm/sqlAdmin/index.js"
106
+ },
107
+ "./sqlAdmin/server": {
108
+ "types": "./esm/sqlAdmin/server/index.d.ts",
109
+ "default": "./esm/sqlAdmin/server/index.js"
101
110
  }
102
111
  },
103
112
  "keywords": [