appflare 0.2.48 → 0.2.49

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 (139) hide show
  1. package/Documentation.md +898 -898
  2. package/cli/commands/index.ts +247 -247
  3. package/cli/generate.ts +360 -360
  4. package/cli/index.ts +120 -120
  5. package/cli/load-config.ts +184 -184
  6. package/cli/schema-compiler.ts +1366 -1366
  7. package/cli/templates/auth/README.md +156 -156
  8. package/cli/templates/auth/config.ts +61 -61
  9. package/cli/templates/auth/route-config.ts +1 -1
  10. package/cli/templates/auth/route-handler.ts +1 -1
  11. package/cli/templates/auth/route-request-utils.ts +5 -5
  12. package/cli/templates/auth/route.config.ts +18 -18
  13. package/cli/templates/auth/route.handler.ts +18 -18
  14. package/cli/templates/auth/route.request-utils.ts +55 -55
  15. package/cli/templates/auth/route.ts +14 -14
  16. package/cli/templates/core/README.md +266 -266
  17. package/cli/templates/core/app-creation.ts +19 -19
  18. package/cli/templates/core/client/appflare.ts +112 -112
  19. package/cli/templates/core/client/handlers/index.ts +763 -763
  20. package/cli/templates/core/client/handlers.ts +1 -1
  21. package/cli/templates/core/client/index.ts +7 -7
  22. package/cli/templates/core/client/storage.ts +195 -195
  23. package/cli/templates/core/client/types.ts +187 -187
  24. package/cli/templates/core/client-modules/appflare.ts +1 -1
  25. package/cli/templates/core/client-modules/handlers.ts +1 -1
  26. package/cli/templates/core/client-modules/index.ts +1 -1
  27. package/cli/templates/core/client-modules/storage.ts +1 -1
  28. package/cli/templates/core/client-modules/types.ts +1 -1
  29. package/cli/templates/core/client.artifacts.ts +39 -39
  30. package/cli/templates/core/client.ts +4 -4
  31. package/cli/templates/core/drizzle.ts +15 -15
  32. package/cli/templates/core/export.ts +14 -14
  33. package/cli/templates/core/handlers.route.ts +24 -24
  34. package/cli/templates/core/handlers.ts +1 -1
  35. package/cli/templates/core/imports.ts +9 -9
  36. package/cli/templates/core/server.ts +38 -38
  37. package/cli/templates/core/types.ts +6 -6
  38. package/cli/templates/core/wrangler.ts +109 -109
  39. package/cli/templates/dashboard/builders/functions/index.ts +17 -17
  40. package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -20
  41. package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -33
  42. package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +271 -271
  43. package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
  44. package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +703 -703
  45. package/cli/templates/dashboard/builders/functions/tree-builder.ts +47 -47
  46. package/cli/templates/dashboard/builders/navigation.ts +155 -155
  47. package/cli/templates/dashboard/builders/storage/index.ts +13 -13
  48. package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
  49. package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
  50. package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
  51. package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
  52. package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
  53. package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
  54. package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
  55. package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
  56. package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
  57. package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
  58. package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
  59. package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
  60. package/cli/templates/dashboard/builders/table-routes/fragments.ts +257 -217
  61. package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
  62. package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
  63. package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
  64. package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
  65. package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
  66. package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
  67. package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
  68. package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
  69. package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
  70. package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
  71. package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
  72. package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
  73. package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
  74. package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
  75. package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
  76. package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
  77. package/cli/templates/dashboard/components/layout.ts +420 -420
  78. package/cli/templates/dashboard/components/login-page.ts +65 -65
  79. package/cli/templates/dashboard/index.ts +61 -61
  80. package/cli/templates/dashboard/types.ts +9 -9
  81. package/cli/templates/handlers/README.md +353 -353
  82. package/cli/templates/handlers/auth.ts +37 -37
  83. package/cli/templates/handlers/execution.ts +44 -42
  84. package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
  85. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
  86. package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
  87. package/cli/templates/handlers/generators/context/storage-api.ts +82 -82
  88. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
  89. package/cli/templates/handlers/generators/context/types.ts +40 -40
  90. package/cli/templates/handlers/generators/context.ts +43 -43
  91. package/cli/templates/handlers/generators/execution.ts +15 -15
  92. package/cli/templates/handlers/generators/handlers.ts +14 -14
  93. package/cli/templates/handlers/generators/registration/modules/cron.ts +35 -35
  94. package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
  95. package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
  96. package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
  97. package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
  98. package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
  99. package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
  100. package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +510 -510
  101. package/cli/templates/handlers/generators/registration/modules/scheduler.ts +65 -65
  102. package/cli/templates/handlers/generators/registration/modules/storage.ts +199 -199
  103. package/cli/templates/handlers/generators/registration/sections.ts +210 -210
  104. package/cli/templates/handlers/generators/types/context.ts +121 -121
  105. package/cli/templates/handlers/generators/types/core.ts +108 -108
  106. package/cli/templates/handlers/generators/types/operations.ts +135 -135
  107. package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +291 -291
  108. package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
  109. package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1382 -1382
  110. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +278 -278
  111. package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
  112. package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
  113. package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
  114. package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +156 -156
  115. package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
  116. package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +958 -958
  117. package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
  118. package/cli/templates/handlers/index.ts +47 -47
  119. package/cli/templates/handlers/operations.ts +116 -116
  120. package/cli/templates/handlers/registration.ts +91 -91
  121. package/cli/templates/handlers/types.ts +17 -17
  122. package/cli/templates/handlers/utils.ts +48 -48
  123. package/cli/types.ts +110 -110
  124. package/cli/utils/handler-discovery.ts +501 -501
  125. package/cli/utils/json-utils.ts +24 -24
  126. package/cli/utils/path-utils.ts +19 -19
  127. package/cli/utils/schema-discovery.ts +402 -399
  128. package/dist/cli/index.js +77 -55
  129. package/dist/cli/index.mjs +77 -55
  130. package/index.ts +18 -18
  131. package/package.json +58 -58
  132. package/react/index.ts +5 -5
  133. package/react/use-infinite-query.ts +255 -255
  134. package/react/use-mutation.ts +89 -89
  135. package/react/use-query.ts +210 -210
  136. package/schema.ts +641 -641
  137. package/test-better-auth-hash.ts +2 -2
  138. package/tsconfig.json +6 -6
  139. package/tsup.config.ts +82 -82
@@ -1,30 +1,30 @@
1
- /**
2
- * Builds the pagination HTML string (runtime template literal) for a given route prefix.
3
- * PocketBase-style minimal footer with total count.
4
- * @param routePrefix - e.g. "/admin/users" or "/admin/table/myTable"
5
- */
6
- export function buildPaginationHtml(routePrefix: string): string {
7
- return `
8
- \t\t<div class="flex flex-col sm:flex-row justify-between items-center mt-4 gap-3 py-3 px-1">
9
- \t\t\t<div class="text-xs text-base-content/40">
10
- \t\t\t\tTotal found: <span class="font-medium text-base-content/60">\${total}</span>
11
- \t\t\t</div>
12
- \t\t\t\${totalPages > 1 ? html\`
13
- \t\t\t<div class="join border border-base-200 rounded-lg">
14
- \t\t\t\t\${page > 1 ? html\`
15
- \t\t\t\t\t<button hx-get="${routePrefix}?page=\${page - 1}&search=\${search}&sort=\${sort}&order=\${order}" hx-target="#main-content" hx-push-url="true" class="join-item btn btn-sm btn-ghost">
16
- \t\t\t\t\t\t<iconify-icon icon="mdi:chevron-left" width="16" height="16"></iconify-icon>
17
- \t\t\t\t\t</button>
18
- \t\t\t\t\` : html\`<button class="join-item btn btn-sm btn-ghost btn-disabled"><iconify-icon icon="mdi:chevron-left" width="16" height="16"></iconify-icon></button>\`}
19
-
20
- \t\t\t\t<button class="join-item btn btn-sm btn-ghost no-animation pointer-events-none text-xs">\${page} / \${totalPages}</button>
21
-
22
- \t\t\t\t\${page < totalPages ? html\`
23
- \t\t\t\t\t<button hx-get="${routePrefix}?page=\${page + 1}&search=\${search}&sort=\${sort}&order=\${order}" hx-target="#main-content" hx-push-url="true" class="join-item btn btn-sm btn-ghost">
24
- \t\t\t\t\t\t<iconify-icon icon="mdi:chevron-right" width="16" height="16"></iconify-icon>
25
- \t\t\t\t\t</button>
26
- \t\t\t\t\` : html\`<button class="join-item btn btn-sm btn-ghost btn-disabled"><iconify-icon icon="mdi:chevron-right" width="16" height="16"></iconify-icon></button>\`}
27
- \t\t\t</div>
28
- \t\t\t\` : ''}
29
- \t\t</div>`;
30
- }
1
+ /**
2
+ * Builds the pagination HTML string (runtime template literal) for a given route prefix.
3
+ * PocketBase-style minimal footer with total count.
4
+ * @param routePrefix - e.g. "/admin/users" or "/admin/table/myTable"
5
+ */
6
+ export function buildPaginationHtml(routePrefix: string): string {
7
+ return `
8
+ \t\t<div class="flex flex-col sm:flex-row justify-between items-center mt-4 gap-3 py-3 px-1">
9
+ \t\t\t<div class="text-xs text-base-content/40">
10
+ \t\t\t\tTotal found: <span class="font-medium text-base-content/60">\${total}</span>
11
+ \t\t\t</div>
12
+ \t\t\t\${totalPages > 1 ? html\`
13
+ \t\t\t<div class="join border border-base-200 rounded-lg">
14
+ \t\t\t\t\${page > 1 ? html\`
15
+ \t\t\t\t\t<button hx-get="${routePrefix}?page=\${page - 1}&search=\${search}&sort=\${sort}&order=\${order}" hx-target="#main-content" hx-push-url="true" class="join-item btn btn-sm btn-ghost">
16
+ \t\t\t\t\t\t<iconify-icon icon="mdi:chevron-left" width="16" height="16"></iconify-icon>
17
+ \t\t\t\t\t</button>
18
+ \t\t\t\t\` : html\`<button class="join-item btn btn-sm btn-ghost btn-disabled"><iconify-icon icon="mdi:chevron-left" width="16" height="16"></iconify-icon></button>\`}
19
+
20
+ \t\t\t\t<button class="join-item btn btn-sm btn-ghost no-animation pointer-events-none text-xs">\${page} / \${totalPages}</button>
21
+
22
+ \t\t\t\t\${page < totalPages ? html\`
23
+ \t\t\t\t\t<button hx-get="${routePrefix}?page=\${page + 1}&search=\${search}&sort=\${sort}&order=\${order}" hx-target="#main-content" hx-push-url="true" class="join-item btn btn-sm btn-ghost">
24
+ \t\t\t\t\t\t<iconify-icon icon="mdi:chevron-right" width="16" height="16"></iconify-icon>
25
+ \t\t\t\t\t</button>
26
+ \t\t\t\t\` : html\`<button class="join-item btn btn-sm btn-ghost btn-disabled"><iconify-icon icon="mdi:chevron-right" width="16" height="16"></iconify-icon></button>\`}
27
+ \t\t\t</div>
28
+ \t\t\t\` : ''}
29
+ \t\t</div>`;
30
+ }
@@ -1,23 +1,23 @@
1
- /**
2
- * Builds the search input HTML string (runtime template literal) for a given route prefix.
3
- * PocketBase-style full-width filter bar.
4
- * @param routePrefix - e.g. "/admin/users" or "/admin/table/myTable"
5
- * @param placeholder - placeholder text for the input
6
- */
7
- export function buildSearchBarHtml(
8
- routePrefix: string,
9
- placeholder = "Search term or filter...",
10
- ): string {
11
- return `
12
- \t\t\t<div class="form-control w-full md:w-auto relative">
13
- \t\t\t\t<iconify-icon icon="mdi:magnify" width="18" height="18" class="absolute left-3 top-1/2 -translate-y-1/2 opacity-40"></iconify-icon>
14
- \t\t\t\t<input type="text"
15
- \t\t\t\t\tname="search"
16
- \t\t\t\t\tplaceholder="${placeholder}"
17
- \t\t\t\t\tvalue="\${search}"
18
- \t\t\t\t\thx-get="${routePrefix}?sort=\${sort}&order=\${order}"
19
- \t\t\t\t\thx-trigger="keyup changed delay:500ms, search"
20
- \t\t\t\t\thx-target="#main-content"
21
- \t\t\t\t\tclass="input input-sm md:input-md input-bordered pl-9 w-full md:w-72 bg-base-200/50 border-base-200 focus:bg-base-100 focus:border-primary transition-all text-sm" />
22
- \t\t\t</div>`;
23
- }
1
+ /**
2
+ * Builds the search input HTML string (runtime template literal) for a given route prefix.
3
+ * PocketBase-style full-width filter bar.
4
+ * @param routePrefix - e.g. "/admin/users" or "/admin/table/myTable"
5
+ * @param placeholder - placeholder text for the input
6
+ */
7
+ export function buildSearchBarHtml(
8
+ routePrefix: string,
9
+ placeholder = "Search term or filter...",
10
+ ): string {
11
+ return `
12
+ \t\t\t<div class="form-control w-full md:w-auto relative">
13
+ \t\t\t\t<iconify-icon icon="mdi:magnify" width="18" height="18" class="absolute left-3 top-1/2 -translate-y-1/2 opacity-40"></iconify-icon>
14
+ \t\t\t\t<input type="text"
15
+ \t\t\t\t\tname="search"
16
+ \t\t\t\t\tplaceholder="${placeholder}"
17
+ \t\t\t\t\tvalue="\${search}"
18
+ \t\t\t\t\thx-get="${routePrefix}?sort=\${sort}&order=\${order}"
19
+ \t\t\t\t\thx-trigger="keyup changed delay:500ms, search"
20
+ \t\t\t\t\thx-target="#main-content"
21
+ \t\t\t\t\tclass="input input-sm md:input-md input-bordered pl-9 w-full md:w-72 bg-base-200/50 border-base-200 focus:bg-base-100 focus:border-primary transition-all text-sm" />
22
+ \t\t\t</div>`;
23
+ }
@@ -1,217 +1,257 @@
1
- import { DiscoveredTable } from "../../types";
2
-
3
- export function buildSearchConditions(table: DiscoveredTable): string {
4
- return table.columns
5
- .filter((column) => column.type === "string")
6
- .map(
7
- (column) => `
8
- \t\t\ttry { searchConditions.push(like(tableSchema.${column.name}, \`%\${search}%\`)); } catch (e) {}
9
- \t\t\t`,
10
- )
11
- .join("");
12
- }
13
-
14
- /**
15
- * Returns an Iconify icon name for a column type.
16
- */
17
- function columnTypeIcon(type: string): string {
18
- switch (type) {
19
- case "number":
20
- return "mdi:pound";
21
- case "boolean":
22
- return "mdi:toggle-switch-outline";
23
- case "date":
24
- return "mdi:calendar";
25
- default:
26
- return "mdi:format-text";
27
- }
28
- }
29
-
30
- export function buildColumnHeaders(
31
- table: DiscoveredTable,
32
- columns: string[],
33
- ): string {
34
- return columns
35
- .map((column) => {
36
- const col = table.columns.find((c) => c.name === column);
37
- const icon = col ? columnTypeIcon(col.type) : "mdi:format-text";
38
- return `
39
- \t\t\t\t\t\t\t<th>
40
- \t\t\t\t\t\t\t\t<a href="#"
41
- \t\t\t\t\t\t\t\t hx-get="/admin/table/${table.exportName}?page=\${page}&search=\${search}&sort=${column}&order=\${sort === '${column}' && order === 'asc' ? 'desc' : 'asc'}"
42
- \t\t\t\t\t\t\t\t hx-target="#main-content"
43
- \t\t\t\t\t\t\t\t hx-push-url="true"
44
- \t\t\t\t\t\t\t\t class="hover:text-primary flex items-center gap-1.5 transition-colors whitespace-nowrap">
45
- \t\t\t\t\t\t\t\t <iconify-icon icon="${icon}" width="14" height="14" class="opacity-40"></iconify-icon>
46
- \t\t\t\t\t\t\t\t ${column}
47
- \t\t\t\t\t\t\t\t <span class="text-[10px] opacity-30">\${sort === '${column}' ? (order === 'asc' ? '▲' : '▼') : ''}</span>
48
- \t\t\t\t\t\t\t\t</a>
49
- \t\t\t\t\t\t\t</th>
50
- \t\t\t\t\t\t\t`;
51
- })
52
- .join("");
53
- }
54
-
55
- export function buildRowCells(columns: string[], primaryKey?: string): string {
56
- return columns
57
- .map((column) => {
58
- if (primaryKey && column === primaryKey) {
59
- return `<td><button type="button" class="truncate max-w-[200px] text-sm font-mono text-xs opacity-70 hover:opacity-100 cursor-copy text-left" title="Click to copy: \${String((row as any).${column} ?? '')}" data-copy-value="\${String((row as any).${column} ?? '')}" onclick="navigator.clipboard?.writeText(this.dataset.copyValue || '')">\${String((row as any).${column} ?? '')}</button></td>`;
60
- }
61
-
62
- return `<td><div class="truncate max-w-[200px] text-sm" title="\${String((row as any).${column} ?? '')}">\${String((row as any).${column} ?? '')}</div></td>`;
63
- })
64
- .join("");
65
- }
66
-
67
- export function buildFieldInput(
68
- table: DiscoveredTable,
69
- columnName: string,
70
- mode: "create" | "edit",
71
- ): string {
72
- const column = table.columns.find((item) => item.name === columnName);
73
- if (!column) {
74
- return "";
75
- }
76
-
77
- const requiredAttr = !column.optional ? " required" : "";
78
- const inputType =
79
- column.type === "number"
80
- ? "number"
81
- : column.type === "date"
82
- ? "date"
83
- : "text";
84
-
85
- if (column.type === "boolean") {
86
- if (mode === "edit") {
87
- return `
88
- \t\t<div class="form-control">
89
- \t\t\t<label class="label cursor-pointer justify-start gap-3">
90
- \t\t\t\t<input type="checkbox" name="${columnName}" value="true" class="checkbox checkbox-sm checkbox-primary" \${(row as any).${columnName} ? 'checked' : ''} />
91
- \t\t\t\t<span class="label-text text-sm">${columnName}</span>
92
- \t\t\t</label>
93
- \t\t</div>
94
- \t\t`;
95
- }
96
-
97
- return `
98
- \t\t<div class="form-control">
99
- \t\t\t<label class="label cursor-pointer justify-start gap-3">
100
- \t\t\t\t<input type="checkbox" name="${columnName}" value="true" class="checkbox checkbox-sm checkbox-primary" />
101
- \t\t\t\t<span class="label-text text-sm">${columnName}</span>
102
- \t\t\t</label>
103
- \t\t</div>
104
- \t\t`;
105
- }
106
-
107
- if (mode === "edit") {
108
- const valueExpression =
109
- column.type === "date"
110
- ? `\${(() => {
111
- const value = (row as any).${columnName};
112
- if (value == null || value === '') return '';
113
-
114
- const date =
115
- value instanceof Date
116
- ? value
117
- : new Date(typeof value === 'number' ? value : String(value));
118
- if (Number.isNaN(date.getTime())) return '';
119
-
120
- const year = date.getFullYear();
121
- const month = String(date.getMonth() + 1).padStart(2, '0');
122
- const day = String(date.getDate()).padStart(2, '0');
123
- return String(year) + '-' + month + '-' + day;
124
- })()}`
125
- : `\${String((row as any).${columnName} ?? '')}`;
126
-
127
- return `
128
- \t\t<div class="form-control">
129
- \t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
130
- \t\t\t<input type="${inputType}" name="${columnName}" class="input input-bordered w-full text-sm" value="${valueExpression}"${requiredAttr} />
131
- \t\t</div>
132
- \t\t`;
133
- }
134
-
135
- return `
136
- \t\t<div class="form-control">
137
- \t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
138
- \t\t\t<input type="${inputType}" name="${columnName}" class="input input-bordered w-full text-sm"${requiredAttr} />
139
- \t\t</div>
140
- \t\t`;
141
- }
142
-
143
- export function buildPayloadAssignments(
144
- table: DiscoveredTable,
145
- fields: string[],
146
- ): string {
147
- return fields
148
- .map((field) => {
149
- const column = table.columns.find((item) => item.name === field);
150
- if (!column) {
151
- return "";
152
- }
153
-
154
- const requiredCheck = !column.optional
155
- ? `
156
- \t\tif (raw_${field} === '') {
157
- \t\t\treturn c.text('${field} is required', 400);
158
- \t\t}
159
- \t\t`
160
- : "";
161
-
162
- if (column.type === "number") {
163
- return `
164
- \t\tconst raw_${field} = getValue(body['${field}']);
165
- \t\t${requiredCheck}
166
- \t\tif (raw_${field} !== '') {
167
- \t\t\tconst parsed_${field} = Number(raw_${field});
168
- \t\t\tif (Number.isNaN(parsed_${field})) {
169
- \t\t\t\treturn c.text('${field} must be a valid number', 400);
170
- \t\t\t}
171
- \t\t\tpayload.${field} = parsed_${field};
172
- \t\t}
173
- \t\t`;
174
- }
175
-
176
- if (column.type === "boolean") {
177
- return `
178
- \t\tconst raw_${field} = getValue(body['${field}']);
179
- \t\tpayload.${field} = raw_${field} === 'true' || raw_${field} === 'on' || raw_${field} === '1';
180
- \t\t`;
181
- }
182
-
183
- if (column.type === "date") {
184
- return `
185
- const raw_${field} = getValue(body['${field}']);
186
- ${requiredCheck}
187
- if (raw_${field} !== '') {
188
- if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw_${field})) {
189
- return c.text('${field} must be a valid date (YYYY-MM-DD)', 400);
190
- }
191
-
192
- const [year_${field}, month_${field}, day_${field}] = raw_${field}.split('-').map(Number);
193
- const parsed_${field} = new Date(year_${field}, month_${field} - 1, day_${field});
194
- if (
195
- Number.isNaN(parsed_${field}.getTime()) ||
196
- parsed_${field}.getFullYear() !== year_${field} ||
197
- parsed_${field}.getMonth() !== month_${field} - 1 ||
198
- parsed_${field}.getDate() !== day_${field}
199
- ) {
200
- return c.text('${field} must be a valid date', 400);
201
- }
202
-
203
- payload.${field} = parsed_${field};
204
- }
205
- `;
206
- }
207
-
208
- return `
209
- \t\tconst raw_${field} = getValue(body['${field}']);
210
- \t\t${requiredCheck}
211
- \t\tif (raw_${field} !== '') {
212
- \t\t\tpayload.${field} = raw_${field};
213
- \t\t}
214
- \t\t`;
215
- })
216
- .join("\n");
217
- }
1
+ import { DiscoveredTable } from "../../types";
2
+
3
+ export function buildSearchConditions(table: DiscoveredTable): string {
4
+ return table.columns
5
+ .filter((column) => column.type === "string")
6
+ .map(
7
+ (column) => `
8
+ \t\t\ttry { searchConditions.push(like(tableSchema.${column.name}, \`%\${search}%\`)); } catch (e) {}
9
+ \t\t\t`,
10
+ )
11
+ .join("");
12
+ }
13
+
14
+ /**
15
+ * Returns an Iconify icon name for a column type.
16
+ */
17
+ function columnTypeIcon(type: string): string {
18
+ switch (type) {
19
+ case "number":
20
+ return "mdi:pound";
21
+ case "boolean":
22
+ return "mdi:toggle-switch-outline";
23
+ case "date":
24
+ return "mdi:calendar";
25
+ case "json":
26
+ return "mdi:code-braces";
27
+ default:
28
+ return "mdi:format-text";
29
+ }
30
+ }
31
+
32
+ export function buildColumnHeaders(
33
+ table: DiscoveredTable,
34
+ columns: string[],
35
+ ): string {
36
+ return columns
37
+ .map((column) => {
38
+ const col = table.columns.find((c) => c.name === column);
39
+ const icon = col ? columnTypeIcon(col.type) : "mdi:format-text";
40
+ return `
41
+ \t\t\t\t\t\t\t<th>
42
+ \t\t\t\t\t\t\t\t<a href="#"
43
+ \t\t\t\t\t\t\t\t hx-get="/admin/table/${table.exportName}?page=\${page}&search=\${search}&sort=${column}&order=\${sort === '${column}' && order === 'asc' ? 'desc' : 'asc'}"
44
+ \t\t\t\t\t\t\t\t hx-target="#main-content"
45
+ \t\t\t\t\t\t\t\t hx-push-url="true"
46
+ \t\t\t\t\t\t\t\t class="hover:text-primary flex items-center gap-1.5 transition-colors whitespace-nowrap">
47
+ \t\t\t\t\t\t\t\t <iconify-icon icon="${icon}" width="14" height="14" class="opacity-40"></iconify-icon>
48
+ \t\t\t\t\t\t\t\t ${column}
49
+ \t\t\t\t\t\t\t\t <span class="text-[10px] opacity-30">\${sort === '${column}' ? (order === 'asc' ? '▲' : '▼') : ''}</span>
50
+ \t\t\t\t\t\t\t\t</a>
51
+ \t\t\t\t\t\t\t</th>
52
+ \t\t\t\t\t\t\t`;
53
+ })
54
+ .join("");
55
+ }
56
+
57
+ export function buildRowCells(table: DiscoveredTable, columns: string[], primaryKey?: string): string {
58
+ return columns
59
+ .map((column) => {
60
+ const col = table.columns.find((c) => c.name === column);
61
+
62
+ if (primaryKey && column === primaryKey) {
63
+ return `<td><button type="button" class="truncate max-w-[200px] text-sm font-mono text-xs opacity-70 hover:opacity-100 cursor-copy text-left" title="Click to copy: \${String((row as any).${column} ?? '')}" data-copy-value="\${String((row as any).${column} ?? '')}" onclick="navigator.clipboard?.writeText(this.dataset.copyValue || '')">\${String((row as any).${column} ?? '')}</button></td>`;
64
+ }
65
+
66
+ if (col?.type === "json") {
67
+ return `<td>\${raw((() => { const __v = (row as any).${column}; if (__v === null || __v === undefined) return '<span class="opacity-30 text-xs">null</span>'; if (Array.isArray(__v)) { if (__v.length === 0) return '<span class="badge badge-ghost badge-sm font-mono text-xs">[ ]</span>'; return '<div class="flex flex-wrap gap-1 max-w-[200px]">' + __v.slice(0, 3).map((item) => '<span class="badge badge-ghost badge-xs font-mono">' + (typeof item === 'object' && item !== null ? JSON.stringify(item).slice(0, 20) + (JSON.stringify(item).length > 20 ? '…' : '') : String(item ?? '')) + '</span>').join('') + (__v.length > 3 ? '<span class="badge badge-ghost badge-xs opacity-50">+' + (__v.length - 3) + '</span>' : '') + '</div>'; } if (typeof __v === 'object') { const __s = JSON.stringify(__v); return '<span class="badge badge-outline badge-sm font-mono text-[10px]">' + __s.slice(0, 50) + (__s.length > 50 ? '…' : '') + '</span>'; } return String(__v ?? ''); })())}</td>`;
68
+ }
69
+
70
+ return `<td><div class="truncate max-w-[200px] text-sm" title="\${String((row as any).${column} ?? '')}">\${String((row as any).${column} ?? '')}</div></td>`;
71
+ })
72
+ .join("");
73
+ }
74
+
75
+ export function buildFieldInput(
76
+ table: DiscoveredTable,
77
+ columnName: string,
78
+ mode: "create" | "edit",
79
+ ): string {
80
+ const column = table.columns.find((item) => item.name === columnName);
81
+ if (!column) {
82
+ return "";
83
+ }
84
+
85
+ const requiredAttr = !column.optional ? " required" : "";
86
+ const inputType =
87
+ column.type === "number"
88
+ ? "number"
89
+ : column.type === "date"
90
+ ? "date"
91
+ : "text";
92
+
93
+ if (column.type === "boolean") {
94
+ if (mode === "edit") {
95
+ return `
96
+ \t\t<div class="form-control">
97
+ \t\t\t<label class="label cursor-pointer justify-start gap-3">
98
+ \t\t\t\t<input type="checkbox" name="${columnName}" value="true" class="checkbox checkbox-sm checkbox-primary" \${(row as any).${columnName} ? 'checked' : ''} />
99
+ \t\t\t\t<span class="label-text text-sm">${columnName}</span>
100
+ \t\t\t</label>
101
+ \t\t</div>
102
+ \t\t`;
103
+ }
104
+
105
+ return `
106
+ \t\t<div class="form-control">
107
+ \t\t\t<label class="label cursor-pointer justify-start gap-3">
108
+ \t\t\t\t<input type="checkbox" name="${columnName}" value="true" class="checkbox checkbox-sm checkbox-primary" />
109
+ \t\t\t\t<span class="label-text text-sm">${columnName}</span>
110
+ \t\t\t</label>
111
+ \t\t</div>
112
+ \t\t`;
113
+ }
114
+
115
+ if (column.type === "json") {
116
+ if (mode === "edit") {
117
+ return `
118
+ \t\t<div class="form-control">
119
+ \t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
120
+ \t\t\t<textarea name="${columnName}" class="textarea textarea-bordered w-full font-mono text-xs h-32"${requiredAttr}>\${typeof (row as any).${columnName} === 'string' ? (row as any).${columnName} : JSON.stringify((row as any).${columnName} ?? null, null, 2)}</textarea>
121
+ \t\t</div>
122
+ \t\t`;
123
+ }
124
+
125
+ return `
126
+ \t\t<div class="form-control">
127
+ \t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
128
+ \t\t\t<textarea name="${columnName}" class="textarea textarea-bordered w-full font-mono text-xs h-32" placeholder="null"${requiredAttr}></textarea>
129
+ \t\t</div>
130
+ \t\t`;
131
+ }
132
+
133
+ if (mode === "edit") {
134
+ const valueExpression =
135
+ column.type === "date"
136
+ ? `\${(() => {
137
+ const value = (row as any).${columnName};
138
+ if (value == null || value === '') return '';
139
+
140
+ const date =
141
+ value instanceof Date
142
+ ? value
143
+ : new Date(typeof value === 'number' ? value : String(value));
144
+ if (Number.isNaN(date.getTime())) return '';
145
+
146
+ const year = date.getFullYear();
147
+ const month = String(date.getMonth() + 1).padStart(2, '0');
148
+ const day = String(date.getDate()).padStart(2, '0');
149
+ return String(year) + '-' + month + '-' + day;
150
+ })()}`
151
+ : `\${String((row as any).${columnName} ?? '')}`;
152
+
153
+ return `
154
+ \t\t<div class="form-control">
155
+ \t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
156
+ \t\t\t<input type="${inputType}" name="${columnName}" class="input input-bordered w-full text-sm" value="${valueExpression}"${requiredAttr} />
157
+ \t\t</div>
158
+ \t\t`;
159
+ }
160
+
161
+ return `
162
+ \t\t<div class="form-control">
163
+ \t\t\t<label class="label"><span class="label-text text-sm font-medium">${columnName}</span></label>
164
+ \t\t\t<input type="${inputType}" name="${columnName}" class="input input-bordered w-full text-sm"${requiredAttr} />
165
+ \t\t</div>
166
+ \t\t`;
167
+ }
168
+
169
+ export function buildPayloadAssignments(
170
+ table: DiscoveredTable,
171
+ fields: string[],
172
+ ): string {
173
+ return fields
174
+ .map((field) => {
175
+ const column = table.columns.find((item) => item.name === field);
176
+ if (!column) {
177
+ return "";
178
+ }
179
+
180
+ const requiredCheck = !column.optional
181
+ ? `
182
+ \t\tif (raw_${field} === '') {
183
+ \t\t\treturn c.text('${field} is required', 400);
184
+ \t\t}
185
+ \t\t`
186
+ : "";
187
+
188
+ if (column.type === "number") {
189
+ return `
190
+ \t\tconst raw_${field} = getValue(body['${field}']);
191
+ \t\t${requiredCheck}
192
+ \t\tif (raw_${field} !== '') {
193
+ \t\t\tconst parsed_${field} = Number(raw_${field});
194
+ \t\t\tif (Number.isNaN(parsed_${field})) {
195
+ \t\t\t\treturn c.text('${field} must be a valid number', 400);
196
+ \t\t\t}
197
+ \t\t\tpayload.${field} = parsed_${field};
198
+ \t\t}
199
+ \t\t`;
200
+ }
201
+
202
+ if (column.type === "boolean") {
203
+ return `
204
+ \t\tconst raw_${field} = getValue(body['${field}']);
205
+ \t\tpayload.${field} = raw_${field} === 'true' || raw_${field} === 'on' || raw_${field} === '1';
206
+ \t\t`;
207
+ }
208
+
209
+ if (column.type === "json") {
210
+ return `
211
+ \t\tconst raw_${field} = getValue(body['${field}']);
212
+ \t\t${requiredCheck}
213
+ \t\tif (raw_${field} !== '') {
214
+ \t\t\ttry {
215
+ \t\t\t\tpayload.${field} = JSON.parse(raw_${field});
216
+ \t\t\t} catch (e) {
217
+ \t\t\t\treturn c.text('${field} must be valid JSON', 400);
218
+ \t\t\t}
219
+ \t\t}
220
+ \t\t`;
221
+ }
222
+
223
+ if (column.type === "date") {
224
+ return `
225
+ const raw_${field} = getValue(body['${field}']);
226
+ ${requiredCheck}
227
+ if (raw_${field} !== '') {
228
+ if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(raw_${field})) {
229
+ return c.text('${field} must be a valid date (YYYY-MM-DD)', 400);
230
+ }
231
+
232
+ const [year_${field}, month_${field}, day_${field}] = raw_${field}.split('-').map(Number);
233
+ const parsed_${field} = new Date(year_${field}, month_${field} - 1, day_${field});
234
+ if (
235
+ Number.isNaN(parsed_${field}.getTime()) ||
236
+ parsed_${field}.getFullYear() !== year_${field} ||
237
+ parsed_${field}.getMonth() !== month_${field} - 1 ||
238
+ parsed_${field}.getDate() !== day_${field}
239
+ ) {
240
+ return c.text('${field} must be a valid date', 400);
241
+ }
242
+
243
+ payload.${field} = parsed_${field};
244
+ }
245
+ `;
246
+ }
247
+
248
+ return `
249
+ \t\tconst raw_${field} = getValue(body['${field}']);
250
+ \t\t${requiredCheck}
251
+ \t\tif (raw_${field} !== '') {
252
+ \t\t\tpayload.${field} = raw_${field};
253
+ \t\t}
254
+ \t\t`;
255
+ })
256
+ .join("\n");
257
+ }