appflare 0.2.7 → 0.2.9

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.
@@ -93,7 +93,6 @@ export async function runMigrate(
93
93
  options: MigrateOptions = {},
94
94
  ): Promise<void> {
95
95
  const loadedConfig = await loadConfig(configPath);
96
- const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
97
96
  const packageDir = findNearestPackageDir(process.cwd());
98
97
  const selectedTargetCount = [
99
98
  Boolean(options.local),
@@ -110,7 +109,7 @@ export async function runMigrate(
110
109
  "drizzle.config.ts",
111
110
  );
112
111
  const drizzleGenerate = Bun.spawn(
113
- [npxCommand, "drizzle-kit", "generate", "--config", drizzleConfigPath],
112
+ ["drizzle-kit", "generate", "--config", drizzleConfigPath],
114
113
  {
115
114
  cwd: packageDir,
116
115
  stdin: "inherit",
@@ -119,7 +118,6 @@ export async function runMigrate(
119
118
  },
120
119
  );
121
120
 
122
- console.log(`npx drizzle-kit generate --config ${drizzleConfigPath}`);
123
121
  const drizzleExitCode = await drizzleGenerate.exited;
124
122
  if (drizzleExitCode !== 0) {
125
123
  throw new Error(
@@ -128,14 +126,7 @@ export async function runMigrate(
128
126
  }
129
127
 
130
128
  const databaseName = loadedConfig.config.database[0].databaseName;
131
- const wranglerArgs = [
132
- npxCommand,
133
- "wrangler",
134
- "d1",
135
- "migrations",
136
- "apply",
137
- databaseName,
138
- ];
129
+ const wranglerArgs = ["wrangler", "d1", "migrations", "apply", databaseName];
139
130
 
140
131
  if (options.local) {
141
132
  wranglerArgs.push("--local");
@@ -171,7 +162,6 @@ export async function runAddAdmin(
171
162
  } = { name: "", email: "", password: "" },
172
163
  ): Promise<void> {
173
164
  const loadedConfig = await loadConfig(configPath);
174
- const npxCommand = process.platform === "win32" ? "npx.cmd" : "npx";
175
165
 
176
166
  const selectedTargetCount = [
177
167
  Boolean(options.local),
@@ -192,22 +182,20 @@ export async function runAddAdmin(
192
182
  const safeName = options.name.replace(/'/g, "''");
193
183
  const safeEmail = options.email.replace(/'/g, "''");
194
184
 
195
- const sqlQuery = `
196
- INSERT INTO users (id, name, email, email_verified, created_at, updated_at, role, banned)
197
- VALUES ('${userId}', '${safeName}', '${safeEmail}', 1, ${now}, ${now}, 'admin', 0);
198
- INSERT INTO accounts (id, account_id, provider_id, user_id, password, created_at, updated_at)
199
- VALUES ('${accountId}', '${safeEmail}', 'credential', '${userId}', '${passwordHash}', ${now}, ${now});
200
- `;
185
+ const sqlQuery = [
186
+ "INSERT INTO users (id, name, email, email_verified, created_at, updated_at, role, banned)",
187
+ `VALUES ('${userId}', '${safeName}', '${safeEmail}', 1, ${now}, ${now}, 'admin', 0);`,
188
+ "INSERT INTO accounts (id, account_id, provider_id, user_id, password, created_at, updated_at)",
189
+ `VALUES ('${accountId}', '${safeEmail}', 'credential', '${userId}', '${passwordHash}', ${now}, ${now});`,
190
+ ].join(" ");
201
191
 
202
192
  const databaseName = loadedConfig.config.database[0].databaseName;
203
193
  const wranglerArgs = [
204
- npxCommand,
205
194
  "wrangler",
206
195
  "d1",
207
196
  "execute",
208
197
  databaseName,
209
- "--command",
210
- sqlQuery,
198
+ `--command=${sqlQuery}`,
211
199
  ];
212
200
 
213
201
  if (options.local) {
@@ -1,11 +1,6 @@
1
1
  import { DiscoveredTable } from "../../../types";
2
- import { buildColumnHeaders, buildRowCells } from "../fragments";
3
2
  import { buildPaginationHtml } from "../common/pagination";
4
3
  import { buildSearchBarHtml } from "../common/search-bar";
5
- import { buildActionsCell } from "./actions-cell";
6
- import { resolvePrimaryKey } from "../helpers";
7
- import { buildFieldInput, buildSearchConditions } from "../fragments";
8
- import { shouldIncludeEditField, shouldIncludeCreateField } from "../helpers";
9
4
 
10
5
  /**
11
6
  * Builds the GET route handler code string for /admin/table/:tableName
@@ -14,6 +9,8 @@ import { shouldIncludeEditField, shouldIncludeCreateField } from "../helpers";
14
9
  export function buildTableGetRoute(
15
10
  table: DiscoveredTable,
16
11
  defaultSort: string,
12
+ primaryKey: string,
13
+ hasPrimaryKey: boolean,
17
14
  columns: string[],
18
15
  searchConditions: string,
19
16
  headers: string,
@@ -28,6 +25,133 @@ export function buildTableGetRoute(
28
25
  `/admin/table/${table.exportName}`,
29
26
  "Search term or filter...",
30
27
  );
28
+ const headerSelectionCell = hasPrimaryKey
29
+ ? `<th class="w-10"><input id="select-all-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" /></th>`
30
+ : `<th class="w-10"><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></th>`;
31
+ const rowSelectionCell = hasPrimaryKey
32
+ ? `<td><input type="checkbox" class="checkbox checkbox-xs row-select-checkbox" value="\${String((row as any).${primaryKey} ?? '')}" /></td>`
33
+ : `<td><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></td>`;
34
+ const bulkDeleteUi = hasPrimaryKey
35
+ ? `
36
+ \t\t\t\t\t\t<div id="bulk-delete-bar-${table.exportName}" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-40 hidden">
37
+ \t\t\t\t\t\t\t<div class="bg-base-100 border border-base-200 rounded-xl shadow-lg px-3 py-2 flex items-center gap-3">
38
+ \t\t\t\t\t\t\t\t<div class="text-xs text-base-content/70">
39
+ \t\t\t\t\t\t\t\t\t<span id="bulk-selected-count-${table.exportName}" class="font-medium text-base-content">0</span> selected
40
+ \t\t\t\t\t\t\t\t</div>
41
+ \t\t\t\t\t\t\t\t<label class="label cursor-pointer gap-2 py-0 px-1">
42
+ \t\t\t\t\t\t\t\t\t<span class="label-text text-xs">All matching (\${total})</span>
43
+ \t\t\t\t\t\t\t\t\t<input id="bulk-all-matching-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" />
44
+ \t\t\t\t\t\t\t\t</label>
45
+ \t\t\t\t\t\t\t\t<button id="bulk-delete-trigger-${table.exportName}" type="button" class="btn btn-error btn-xs gap-1">
46
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:delete-outline" width="14" height="14"></iconify-icon>
47
+ \t\t\t\t\t\t\t\t\tDelete selected
48
+ \t\t\t\t\t\t\t\t</button>
49
+ \t\t\t\t\t\t\t</div>
50
+ \t\t\t\t\t\t</div>
51
+
52
+ \t\t\t\t\t\t<dialog id="bulk-delete-modal-${table.exportName}" class="modal">
53
+ \t\t\t\t\t\t\t<div class="modal-box max-w-sm p-6 space-y-4">
54
+ \t\t\t\t\t\t\t\t<h3 class="font-semibold text-base">Delete selected rows?</h3>
55
+ \t\t\t\t\t\t\t\t<p class="text-sm text-base-content/70" id="bulk-delete-description-${table.exportName}">This action cannot be undone.</p>
56
+ \t\t\t\t\t\t\t\t<form id="bulk-delete-form-${table.exportName}" hx-post="/admin/table/${table.exportName}/delete-bulk" hx-target="#main-content" hx-swap="outerHTML" class="space-y-3">
57
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="bulkMode" id="bulk-delete-mode-${table.exportName}" value="selected" />
58
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="selectedIds" id="bulk-delete-ids-${table.exportName}" value="" />
59
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
60
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
61
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
62
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
63
+ \t\t\t\t\t\t\t\t\t<div class="flex justify-end gap-2 pt-1">
64
+ \t\t\t\t\t\t\t\t\t\t<button type="button" class="btn btn-ghost btn-sm" onclick="this.closest('dialog')?.close()">Cancel</button>
65
+ \t\t\t\t\t\t\t\t\t\t<button type="submit" class="btn btn-error btn-sm">Delete</button>
66
+ \t\t\t\t\t\t\t\t\t</div>
67
+ \t\t\t\t\t\t\t\t</form>
68
+ \t\t\t\t\t\t\t</div>
69
+ \t\t\t\t\t\t\t<form method="dialog" class="modal-backdrop"><button>close</button></form>
70
+ \t\t\t\t\t\t</dialog>
71
+
72
+ \t\t\t\t\t\t<script>
73
+ \t\t\t\t\t\t\t(() => {
74
+ \t\t\t\t\t\t\t\tconst container = document.querySelector('#main-content');
75
+ \t\t\t\t\t\t\t\tif (!container) return;
76
+
77
+ \t\t\t\t\t\t\t\tconst rowCheckboxes = Array.from(container.querySelectorAll('.row-select-checkbox')).filter((node) => node instanceof HTMLInputElement);
78
+ \t\t\t\t\t\t\t\tif (rowCheckboxes.length === 0) return;
79
+
80
+ \t\t\t\t\t\t\t\tconst selectAll = container.querySelector('#select-all-${table.exportName}');
81
+ \t\t\t\t\t\t\t\tconst bar = container.querySelector('#bulk-delete-bar-${table.exportName}');
82
+ \t\t\t\t\t\t\t\tconst selectedCount = container.querySelector('#bulk-selected-count-${table.exportName}');
83
+ \t\t\t\t\t\t\t\tconst allMatching = container.querySelector('#bulk-all-matching-${table.exportName}');
84
+ \t\t\t\t\t\t\t\tconst trigger = container.querySelector('#bulk-delete-trigger-${table.exportName}');
85
+ \t\t\t\t\t\t\t\tconst modal = container.querySelector('#bulk-delete-modal-${table.exportName}');
86
+ \t\t\t\t\t\t\t\tconst modeInput = container.querySelector('#bulk-delete-mode-${table.exportName}');
87
+ \t\t\t\t\t\t\t\tconst idsInput = container.querySelector('#bulk-delete-ids-${table.exportName}');
88
+ \t\t\t\t\t\t\t\tconst description = container.querySelector('#bulk-delete-description-${table.exportName}');
89
+
90
+ \t\t\t\t\t\t\t\tif (!(selectAll instanceof HTMLInputElement) ||
91
+ \t\t\t\t\t\t\t\t\t!(bar instanceof HTMLElement) ||
92
+ \t\t\t\t\t\t\t\t\t!(selectedCount instanceof HTMLElement) ||
93
+ \t\t\t\t\t\t\t\t\t!(allMatching instanceof HTMLInputElement) ||
94
+ \t\t\t\t\t\t\t\t\t!(trigger instanceof HTMLButtonElement) ||
95
+ \t\t\t\t\t\t\t\t\t!(modal instanceof HTMLDialogElement) ||
96
+ \t\t\t\t\t\t\t\t\t!(modeInput instanceof HTMLInputElement) ||
97
+ \t\t\t\t\t\t\t\t\t!(idsInput instanceof HTMLInputElement) ||
98
+ \t\t\t\t\t\t\t\t\t!(description instanceof HTMLElement)) {
99
+ \t\t\t\t\t\t\t\t\treturn;
100
+ \t\t\t\t\t\t\t\t}
101
+
102
+ \t\t\t\t\t\t\t\tconst totalRows = Number(\${total});
103
+ \t\t\t\t\t\t\t\tconst getSelectedIds = () => rowCheckboxes.filter((checkbox) => checkbox.checked).map((checkbox) => checkbox.value).filter((value) => value.length > 0);
104
+
105
+ \t\t\t\t\t\t\t\tconst updateUi = () => {
106
+ \t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
107
+ \t\t\t\t\t\t\t\t\tconst selected = allMatching.checked ? totalRows : ids.length;
108
+ \t\t\t\t\t\t\t\t\tselectedCount.textContent = String(selected);
109
+ \t\t\t\t\t\t\t\t\tbar.classList.toggle('hidden', selected === 0);
110
+ \t\t\t\t\t\t\t\t\tconst checkedCount = ids.length;
111
+ \t\t\t\t\t\t\t\t\tselectAll.checked = checkedCount > 0 && checkedCount === rowCheckboxes.length;
112
+ \t\t\t\t\t\t\t\t\tselectAll.indeterminate = checkedCount > 0 && checkedCount < rowCheckboxes.length;
113
+ \t\t\t\t\t\t\t\t};
114
+
115
+ \t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
116
+ \t\t\t\t\t\t\t\t\tcheckbox.addEventListener('change', () => {
117
+ \t\t\t\t\t\t\t\t\t\tif (allMatching.checked) allMatching.checked = false;
118
+ \t\t\t\t\t\t\t\t\t\tupdateUi();
119
+ \t\t\t\t\t\t\t\t\t});
120
+ \t\t\t\t\t\t\t\t});
121
+
122
+ \t\t\t\t\t\t\t\tselectAll.addEventListener('change', () => {
123
+ \t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
124
+ \t\t\t\t\t\t\t\t\t\tcheckbox.checked = selectAll.checked;
125
+ \t\t\t\t\t\t\t\t\t});
126
+ \t\t\t\t\t\t\t\t\tif (allMatching.checked && !selectAll.checked) allMatching.checked = false;
127
+ \t\t\t\t\t\t\t\t\tupdateUi();
128
+ \t\t\t\t\t\t\t\t});
129
+
130
+ \t\t\t\t\t\t\t\tallMatching.addEventListener('change', () => {
131
+ \t\t\t\t\t\t\t\t\tif (allMatching.checked) {
132
+ \t\t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
133
+ \t\t\t\t\t\t\t\t\t\t\tcheckbox.checked = true;
134
+ \t\t\t\t\t\t\t\t\t\t});
135
+ \t\t\t\t\t\t\t\t\t}
136
+ \t\t\t\t\t\t\t\t\tupdateUi();
137
+ \t\t\t\t\t\t\t\t});
138
+
139
+ \t\t\t\t\t\t\t\ttrigger.addEventListener('click', () => {
140
+ \t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
141
+ \t\t\t\t\t\t\t\t\tif (!allMatching.checked && ids.length === 0) return;
142
+ \t\t\t\t\t\t\t\t\tmodeInput.value = allMatching.checked ? 'all-matching' : 'selected';
143
+ \t\t\t\t\t\t\t\t\tidsInput.value = ids.join(',');
144
+ \t\t\t\t\t\t\t\t\tdescription.textContent = allMatching.checked
145
+ \t\t\t\t\t\t\t\t\t\t? 'This will permanently delete all rows matching the current search context.'
146
+ \t\t\t\t\t\t\t\t\t\t: 'This action cannot be undone.';
147
+ \t\t\t\t\t\t\t\t\tmodal.showModal();
148
+ \t\t\t\t\t\t\t\t});
149
+
150
+ \t\t\t\t\t\t\t\tupdateUi();
151
+ \t\t\t\t\t\t\t})();
152
+ \t\t\t\t\t\t</script>
153
+ `
154
+ : "";
31
155
 
32
156
  return `
33
157
  \tadminApp.get('/table/${table.exportName}', async (c) => {
@@ -69,7 +193,7 @@ export function buildTableGetRoute(
69
193
  \t\t\t\t\t<table class="table table-sm md:table-md w-full">
70
194
  \t\t\t\t\t\t<thead>
71
195
  \t\t\t\t\t\t\t<tr class="border-b border-base-200">
72
- \t\t\t\t\t\t\t\t<th class="w-10"><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></th>
196
+ \t\t\t\t\t\t\t\t${headerSelectionCell}
73
197
  \t\t\t\t\t\t\t\t${headers}
74
198
  \t\t\t\t\t\t\t\t<th class="w-[100px] text-right">
75
199
  \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:dots-horizontal" width="16" height="16" class="opacity-30"></iconify-icon>
@@ -79,7 +203,7 @@ export function buildTableGetRoute(
79
203
  \t\t\t\t\t\t<tbody>
80
204
  \t\t\t\t\t\t\t\${data.map((row, rowIndex) => html\`
81
205
  \t\t\t\t\t\t\t<tr class="hover:bg-base-200/30 transition-colors">
82
- \t\t\t\t\t\t\t\t<td><input type="checkbox" class="checkbox checkbox-xs" /></td>
206
+ \t\t\t\t\t\t\t\t${rowSelectionCell}
83
207
  \t\t\t\t\t\t\t\t${rowCells}
84
208
  \t\t\t\t\t\t\t\t${actionsCell}
85
209
  \t\t\t\t\t\t\t</tr>
@@ -130,6 +254,7 @@ export function buildTableGetRoute(
130
254
  \t\t\t\t\t\t\t${searchBarHtml}
131
255
  \t\t\t\t\t\t</div>
132
256
  \t\t\t\t\t\t\${tableHtml}
257
+ \t\t\t\t\t\t${bulkDeleteUi}
133
258
  \t\t\t\t\t</div>
134
259
  \t\t\t\t\t<div class="drawer-side z-50">
135
260
  \t\t\t\t\t\t<label for="create-drawer-${table.exportName}" aria-label="close sidebar" class="drawer-overlay"></label>
@@ -56,6 +56,8 @@ export function buildTableRoute(table: DiscoveredTable): string {
56
56
  buildTableGetRoute(
57
57
  table,
58
58
  defaultSort,
59
+ primaryKey,
60
+ hasPrimaryKey,
59
61
  columns,
60
62
  searchConditions,
61
63
  headers,
@@ -70,6 +72,7 @@ export function buildTableRoute(table: DiscoveredTable): string {
70
72
  primaryKey,
71
73
  primaryKeyType,
72
74
  hasPrimaryKey,
75
+ searchConditions,
73
76
  createAssignments,
74
77
  editAssignments,
75
78
  )
@@ -8,6 +8,7 @@ export function buildTablePostRoutes(
8
8
  primaryKey: string,
9
9
  primaryKeyType: string | undefined,
10
10
  hasPrimaryKey: boolean,
11
+ searchConditions: string,
11
12
  createAssignments: string,
12
13
  editAssignments: string,
13
14
  ): string {
@@ -20,6 +21,11 @@ export function buildTablePostRoutes(
20
21
  \t\t`
21
22
  : "";
22
23
 
24
+ const parseBulkIds =
25
+ primaryKeyType === "number"
26
+ ? "idValues.map((value) => Number(value)).filter((value) => !Number.isNaN(value))"
27
+ : "idValues";
28
+
23
29
  const editRoute = hasPrimaryKey
24
30
  ? `
25
31
  \tadminApp.post('/table/${exportName}/edit', async (c) => {
@@ -74,6 +80,52 @@ export function buildTablePostRoutes(
74
80
  \t\t\t.where(eq(tableSchema.${primaryKey}, idValue as any))
75
81
  \t\t\t.execute();
76
82
 
83
+ \t\tconst query = new URLSearchParams({
84
+ \t\t\tpage: getValue(body.page) || '1',
85
+ \t\t\tsort: getValue(body.sort) || '${defaultSort}',
86
+ \t\t\torder: getValue(body.order) || 'desc',
87
+ \t\t\tsearch: getValue(body.search) || '',
88
+ \t\t});
89
+ \t\treturn c.redirect('/admin/table/${exportName}?' + query.toString());
90
+ \t});
91
+
92
+ \tadminApp.post('/table/${exportName}/delete-bulk', async (c) => {
93
+ \t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
94
+ \t\tconst tableSchema = (schema as any).${exportName};
95
+ \t\tif (!tableSchema) return c.text('Table missing', 404);
96
+
97
+ \t\tconst body = await c.req.parseBody();
98
+ \t\tconst getValue = (value: unknown) => (typeof value === 'string' ? value : '');
99
+ \t\tconst mode = getValue(body.bulkMode);
100
+ \t\tconst selectedIdsRaw = getValue(body.selectedIds);
101
+ \t\tconst search = getValue(body.search);
102
+
103
+ \t\tif (mode === 'all-matching') {
104
+ \t\t\tlet deleteQuery = db.delete(tableSchema);
105
+ \t\t\tif (search) {
106
+ \t\t\t\tconst searchConditions = [];
107
+ \t\t\t\t${searchConditions}
108
+ \t\t\t\tif (searchConditions.length > 0) {
109
+ \t\t\t\t\tdeleteQuery = deleteQuery.where(or(...searchConditions)) as any;
110
+ \t\t\t\t}
111
+ \t\t\t}
112
+ \t\t\tawait deleteQuery.execute();
113
+ \t\t} else {
114
+ \t\t\tconst idValues = selectedIdsRaw
115
+ \t\t\t\t.split(',')
116
+ \t\t\t\t.map((value) => value.trim())
117
+ \t\t\t\t.filter((value) => value.length > 0);
118
+ \t\t\tif (idValues.length === 0) return c.text('No rows selected', 400);
119
+
120
+ \t\t\tconst parsedIds: unknown[] = ${parseBulkIds};
121
+ \t\t\tif (parsedIds.length === 0) return c.text('No valid selected rows', 400);
122
+
123
+ \t\t\tawait db
124
+ \t\t\t\t.delete(tableSchema)
125
+ \t\t\t\t.where(inArray(tableSchema.${primaryKey}, parsedIds as any))
126
+ \t\t\t\t.execute();
127
+ \t\t}
128
+
77
129
  \t\tconst query = new URLSearchParams({
78
130
  \t\t\tpage: getValue(body.page) || '1',
79
131
  \t\t\tsort: getValue(body.sort) || '${defaultSort}',
@@ -31,7 +31,7 @@ export function generateDashboardSource(
31
31
  return `import { Hono } from "hono";
32
32
  import { html, raw } from "hono/html";
33
33
  import { drizzle } from "drizzle-orm/d1";
34
- import { eq, desc, asc, sql, like, or } from "drizzle-orm";
34
+ import { eq, desc, asc, sql, like, or, inArray } from "drizzle-orm";
35
35
  import { createAuth } from "./auth.config";
36
36
  import * as schema from "${schemaImportPath}";
37
37
  import { users } from "./auth.schema";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appflare",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "bin": {
5
5
  "appflare": "./cli/index.ts"
6
6
  },